Commit 607d164380313624531a09b31b6c65ac4c670ee7

Vicent Martí 2011-06-15T17:24:04

Merge pull request #248 from carlosmn/config Implement config writing

diff --git a/src/config_file.c b/src/config_file.c
index d76c602..916b4d0 100644
--- a/src/config_file.c
+++ b/src/config_file.c
@@ -26,9 +26,11 @@
 #include "common.h"
 #include "config.h"
 #include "fileops.h"
+#include "filebuf.h"
 #include "git2/config.h"
 #include "git2/types.h"
 
+
 #include <ctype.h>
 
 typedef struct cvar_t {
@@ -98,6 +100,7 @@ typedef struct {
 
 static int config_parse(diskfile_backend *cfg_file);
 static int parse_variable(diskfile_backend *cfg, char **var_name, char **var_value);
+static int config_write(diskfile_backend *cfg, cvar_t *var);
 
 static void cvar_free(cvar_t *var)
 {
@@ -130,7 +133,7 @@ static void cvar_list_free(cvar_t_list *list)
  */
 static int cvar_match_section(const char *local, const char *input)
 {
-	char *first_dot, *last_dot;
+	char *first_dot;
 	char *local_sp = strchr(local, ' ');
 	int comparison_len;
 
@@ -159,12 +162,8 @@ static int cvar_match_section(const char *local, const char *input)
 	 */
 
 	first_dot = strchr(input, '.');
-	last_dot = strrchr(input, '.');
 	comparison_len = strlen(local_sp + 2) - 1;
 
-	if (last_dot == first_dot || last_dot - first_dot - 1 != comparison_len)
-		return 0;
-
 	return !strncmp(local_sp + 2, first_dot + 1, comparison_len);
 }
 
@@ -241,6 +240,39 @@ static int cvar_normalize_name(cvar_t *var, char **output)
 	return GIT_SUCCESS;
 }
 
+static char  *interiorize_section(const char *orig)
+{
+	char *dot, *last_dot, *section, *ret;
+	int len;
+
+	dot = strchr(orig, '.');
+	last_dot = strrchr(orig, '.');
+	len = last_dot - orig;
+
+	/* No subsection, this is easy */
+	if (last_dot == dot)
+		return git__strndup(orig, dot - orig);
+
+	section = git__malloc(len + 4);
+	if (section == NULL)
+		return NULL;
+
+	memset(section, 0x0, len + 4);
+	ret = section;
+	len = dot - orig;
+	memcpy(section, orig, len);
+	section += len;
+	len = STRLEN(" \"");
+	memcpy(section, " \"", len);
+	section += len;
+	len = last_dot - dot - 1;
+	memcpy(section, dot + 1, len);
+	section += len;
+	*section = '"';
+
+	return ret;
+}
+
 static int config_open(git_config_file *cfg)
 {
 	int error;
@@ -320,7 +352,7 @@ static int config_set(git_config_file *cfg, const char *name, const char *value)
 		free(existing->value);
 		existing->value = tmp;
 
-		return GIT_SUCCESS;
+		return config_write(b, existing);
 	}
 
 	/*
@@ -338,7 +370,7 @@ static int config_set(git_config_file *cfg, const char *name, const char *value)
 
 	memset(var, 0x0, sizeof(cvar_t));
 
-	var->section = git__strndup(name, last_dot - name);
+	var->section = interiorize_section(name);
 	if (var->section == NULL) {
 		error = GIT_ENOMEM;
 		goto out;
@@ -357,6 +389,7 @@ static int config_set(git_config_file *cfg, const char *name, const char *value)
 	}
 
 	CVAR_LIST_APPEND(&b->var_list, var);
+	error = config_write(b, var);
 
  out:
 	if (error < GIT_SUCCESS)
@@ -863,6 +896,173 @@ static int config_parse(diskfile_backend *cfg_file)
 	return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to parse config");
 }
 
+static int write_section(git_filebuf *file, cvar_t *var)
+{
+	int error;
+
+	error = git_filebuf_printf(file, "[%s]\n", var->section);
+	if (error < GIT_SUCCESS)
+		return error;
+
+	error = git_filebuf_printf(file, "    %s = %s\n", var->name, var->value);
+	return error;
+}
+
+/*
+ * This is pretty much the parsing, except we write out anything we don't have
+ */
+static int config_write(diskfile_backend *cfg, cvar_t *var)
+{
+	int error = GIT_SUCCESS, c;
+	int section_matches = 0, last_section_matched = 0;
+	char *current_section = NULL;
+	char *var_name, *var_value, *data_start;
+	git_filebuf file;
+	const char *pre_end = NULL, *post_start = NULL;
+
+	/* We need to read in our own config file */
+	error = gitfo_read_file(&cfg->reader.buffer, cfg->file_path);
+	if (error < GIT_SUCCESS) {
+		return git__rethrow(error, "Failed to read existing config file %s", cfg->file_path);
+	}
+
+	/* Initialise the reading position */
+	cfg->reader.read_ptr = cfg->reader.buffer.data;
+	cfg->reader.eof = 0;
+	data_start = cfg->reader.read_ptr;
+
+	/* Lock the file */
+	error = git_filebuf_open(&file, cfg->file_path, 0);
+	if (error < GIT_SUCCESS)
+		return git__rethrow(error, "Failed to lock config file");
+
+	skip_bom(cfg);
+
+	while (error == GIT_SUCCESS && !cfg->reader.eof) {
+		c = cfg_peek(cfg, SKIP_WHITESPACE);
+
+		switch (c) {
+		case '\0': /* We've arrived at the end of the file */
+			break;
+
+		case '[': /* section header, new section begins */
+			/*
+			 * We set both positions to the current one in case we
+			 * need to add a variable to the end of a section. In that
+			 * case, we want both variables to point just before the
+			 * new section. If we actually want to replace it, the
+			 * default case will take care of updating them.
+			 */
+			pre_end = post_start = cfg->reader.read_ptr;
+			free(current_section);
+			error = parse_section_header(cfg, &current_section);
+			if (error < GIT_SUCCESS)
+				break;
+
+			/* Keep track of when it stops matching */
+			last_section_matched = section_matches;
+			section_matches = !strcmp(current_section, var->section);
+			break;
+
+		case ';':
+		case '#':
+			cfg_consume_line(cfg);
+			break;
+
+		default:
+			/*
+			 * If the section doesn't match, but the last section did,
+			 * it means we need to add a variable (so skip the line
+			 * otherwise). If both the section and name match, we need
+			 * to overwrite the variable (so skip the line
+			 * otherwise). pre_end needs to be updated each time so we
+			 * don't loose that information, but we only need to
+			 * update post_start if we're going to use it in this
+			 * iteration.
+			 */
+			if (!section_matches) {
+				if (!last_section_matched) {
+					cfg_consume_line(cfg);
+					break;
+				}
+			} else {
+				pre_end = cfg->reader.read_ptr;
+				error = parse_variable(cfg, &var_name, &var_value);
+				if (error < GIT_SUCCESS || strcasecmp(var->name, var_name))
+					break;
+				post_start = cfg->reader.read_ptr;
+			}
+
+			/*
+			 * We've found the variable we wanted to change, so
+			 * write anything up to it
+			 */
+			error = git_filebuf_write(&file, data_start, pre_end - data_start);
+			if (error < GIT_SUCCESS) {
+				git__rethrow(error, "Failed to write the first part of the file");
+				break;
+			}
+
+			/* Then replace the variable */
+			error = git_filebuf_printf(&file, "\t%s = %s\n", var->name, var->value);
+			if (error < GIT_SUCCESS) {
+				git__rethrow(error, "Failed to overwrite the variable");
+				break;
+			}
+
+			/* And then the write out rest of the file */
+			error = git_filebuf_write(&file, post_start,
+			            cfg->reader.buffer.len - (post_start - data_start));
+
+			if (error < GIT_SUCCESS) {
+				git__rethrow(error, "Failed to write the rest of the file");
+					break;
+			}
+
+			goto cleanup;
+		}
+	}
+
+	/*
+	 * Being here can mean that
+	 *
+	 * 1) our section is the last one in the file and we're
+	 * adding a variable
+	 *
+	 * 2) we didn't find a section for us so we need to create it
+	 * ourselves.
+	 *
+	 * Either way we need to write out the whole file.
+	 */
+
+	error = git_filebuf_write(&file, cfg->reader.buffer.data, cfg->reader.buffer.len);
+	if (error < GIT_SUCCESS) {
+		git__rethrow(error, "Failed to write original config content");
+		goto cleanup;
+	}
+
+	/* And now if we just need to add a variable */
+	if (section_matches) {
+		error = git_filebuf_printf(&file, "\t%s = %s\n", var->name, var->value);
+		goto cleanup;
+	}
+
+	/* Or maybe we need to write out a whole section */
+	error = write_section(&file, var);
+	if (error < GIT_SUCCESS)
+		git__rethrow(error, "Failed to write new section");
+
+ cleanup:
+	free(current_section);
+
+	if (error < GIT_SUCCESS)
+		git_filebuf_cleanup(&file);
+	else
+		error = git_filebuf_commit(&file);
+
+	return error;
+}
+
 static int is_multiline_var(const char *str)
 {
 	char *end = strrchr(str, '\0') - 1;
diff --git a/tests/resources/config/config9 b/tests/resources/config/config9
new file mode 100644
index 0000000..4359c78
--- /dev/null
+++ b/tests/resources/config/config9
@@ -0,0 +1,2 @@
+[core]
+	dummy = 1
diff --git a/tests/t15-config.c b/tests/t15-config.c
index 08a2cdb..c11c5a9 100644
--- a/tests/t15-config.c
+++ b/tests/t15-config.c
@@ -189,6 +189,27 @@ BEGIN_TEST(config8, "don't fail on empty files")
 	git_config_free(cfg);
 END_TEST
 
+BEGIN_TEST
+(config9, "replace a value")
+	git_config *cfg;
+	int i;
+
+	/* By freeing the config, we make sure we flush the values  */
+	must_pass(git_config_open_file(&cfg, CONFIG_BASE "/config9"));
+	must_pass(git_config_set_int(cfg, "core.dummy", 5));
+	git_config_free(cfg);
+
+	must_pass(git_config_open_file(&cfg, CONFIG_BASE "/config9"));
+	must_pass(git_config_get_int(cfg, "core.dummy", &i));
+	must_be_true(i == 5);
+	git_config_free(cfg);
+
+	must_pass(git_config_open_file(&cfg, CONFIG_BASE "/config9"));
+	must_pass(git_config_set_int(cfg, "core.dummy", 1));
+	git_config_free(cfg);
+
+END_TEST
+
 BEGIN_SUITE(config)
 	 ADD_TEST(config0);
 	 ADD_TEST(config1);
@@ -199,4 +220,5 @@ BEGIN_SUITE(config)
 	 ADD_TEST(config6);
 	 ADD_TEST(config7);
 	 ADD_TEST(config8);
+	 ADD_TEST(config9);
 END_SUITE