Commit 074d323f by Carlos Martín Nieto

Merge pull request #3079 from ethomson/config

Configuration changes for handling multiple of the same sections
parents c3414d53 d6b7e404
...@@ -114,8 +114,7 @@ typedef struct { ...@@ -114,8 +114,7 @@ typedef struct {
diskfile_backend *snapshot_from; diskfile_backend *snapshot_from;
} diskfile_readonly_backend; } diskfile_readonly_backend;
static int config_parse(git_strmap *values, diskfile_backend *cfg_file, struct reader *reader, git_config_level_t level, int depth); static int config_read(git_strmap *values, diskfile_backend *cfg_file, struct reader *reader, git_config_level_t level, int depth);
static int parse_variable(struct reader *reader, char **var_name, char **var_value);
static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char *value); static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char *value);
static char *escape_value(const char *ptr); static char *escape_value(const char *ptr);
...@@ -288,7 +287,7 @@ static int config_open(git_config_backend *cfg, git_config_level_t level) ...@@ -288,7 +287,7 @@ static int config_open(git_config_backend *cfg, git_config_level_t level)
if (res == GIT_ENOTFOUND) if (res == GIT_ENOTFOUND)
return 0; return 0;
if (res < 0 || (res = config_parse(b->header.values->values, b, reader, level, 0)) < 0) { if (res < 0 || (res = config_read(b->header.values->values, b, reader, level, 0)) < 0) {
refcounted_strmap_free(b->header.values); refcounted_strmap_free(b->header.values);
b->header.values = NULL; b->header.values = NULL;
} }
...@@ -313,7 +312,7 @@ static int config__refresh(git_config_backend *cfg) ...@@ -313,7 +312,7 @@ static int config__refresh(git_config_backend *cfg)
reader = git_array_get(b->readers, git_array_size(b->readers) - 1); reader = git_array_get(b->readers, git_array_size(b->readers) - 1);
GITERR_CHECK_ALLOC(reader); GITERR_CHECK_ALLOC(reader);
if ((error = config_parse(values->values, b, reader, b->level, 0)) < 0) if ((error = config_read(values->values, b, reader, b->level, 0)) < 0)
goto out; goto out;
git_mutex_lock(&b->header.values_mutex); git_mutex_lock(&b->header.values_mutex);
...@@ -831,7 +830,7 @@ static int reader_getchar_raw(struct reader *reader) ...@@ -831,7 +830,7 @@ static int reader_getchar_raw(struct reader *reader)
if (c == 0) { if (c == 0) {
reader->eof = 1; reader->eof = 1;
c = '\n'; c = '\0';
} }
return c; return c;
...@@ -850,13 +849,12 @@ static int reader_getchar(struct reader *reader, int flags) ...@@ -850,13 +849,12 @@ static int reader_getchar(struct reader *reader, int flags)
do { do {
c = reader_getchar_raw(reader); c = reader_getchar_raw(reader);
} while (skip_whitespace && git__isspace(c) && } while (c != '\n' && c != '\0' && skip_whitespace && git__isspace(c));
!reader->eof);
if (skip_comments && (c == '#' || c == ';')) { if (skip_comments && (c == '#' || c == ';')) {
do { do {
c = reader_getchar_raw(reader); c = reader_getchar_raw(reader);
} while (c != '\n'); } while (c != '\n' && c != '\0');
} }
return c; return c;
...@@ -1200,398 +1198,6 @@ static int included_path(git_buf *out, const char *dir, const char *path) ...@@ -1200,398 +1198,6 @@ static int included_path(git_buf *out, const char *dir, const char *path)
return git_path_join_unrooted(out, path, dir, NULL); return git_path_join_unrooted(out, path, dir, NULL);
} }
static int config_parse(git_strmap *values, diskfile_backend *cfg_file, struct reader *reader, git_config_level_t level, int depth)
{
int c;
char *current_section = NULL;
char *var_name;
char *var_value;
cvar_t *var;
git_buf buf = GIT_BUF_INIT;
int result = 0;
uint32_t reader_idx;
if (depth >= MAX_INCLUDE_DEPTH) {
giterr_set(GITERR_CONFIG, "Maximum config include depth reached");
return -1;
}
reader_idx = git_array_size(cfg_file->readers) - 1;
/* Initialize the reading position */
reader->read_ptr = reader->buffer.ptr;
reader->eof = 0;
/* If the file is empty, there's nothing for us to do */
if (*reader->read_ptr == '\0')
return 0;
skip_bom(reader);
while (result == 0 && !reader->eof) {
c = reader_peek(reader, SKIP_WHITESPACE);
switch (c) {
case '\n': /* EOF when peeking, set EOF in the reader to exit the loop */
reader->eof = 1;
break;
case '[': /* section header, new section begins */
git__free(current_section);
current_section = NULL;
result = parse_section_header(reader, &current_section);
break;
case ';':
case '#':
reader_consume_line(reader);
break;
default: /* assume variable declaration */
result = parse_variable(reader, &var_name, &var_value);
if (result < 0)
break;
git__strtolower(var_name);
git_buf_printf(&buf, "%s.%s", current_section, var_name);
git__free(var_name);
if (git_buf_oom(&buf)) {
git__free(var_value);
return -1;
}
var = git__calloc(1, sizeof(cvar_t));
GITERR_CHECK_ALLOC(var);
var->entry = git__calloc(1, sizeof(git_config_entry));
GITERR_CHECK_ALLOC(var->entry);
var->entry->name = git_buf_detach(&buf);
var->entry->value = var_value;
var->entry->level = level;
var->included = !!depth;
if ((result = append_entry(values, var)) < 0)
break;
else
result = 0;
/* Add or append the new config option */
if (!git__strcmp(var->entry->name, "include.path")) {
struct reader *r;
git_buf path = GIT_BUF_INIT;
char *dir;
uint32_t index;
r = git_array_alloc(cfg_file->readers);
/* The reader may have been reallocated */
reader = git_array_get(cfg_file->readers, reader_idx);
memset(r, 0, sizeof(struct reader));
if ((result = git_path_dirname_r(&path, reader->file_path)) < 0)
break;
/* We need to know our index in the array, as the next config_parse call may realloc */
index = git_array_size(cfg_file->readers) - 1;
dir = git_buf_detach(&path);
result = included_path(&path, dir, var->entry->value);
git__free(dir);
if (result < 0)
break;
r->file_path = git_buf_detach(&path);
git_buf_init(&r->buffer, 0);
result = git_futils_readbuffer_updated(&r->buffer, r->file_path, &r->file_mtime,
&r->file_size, NULL);
if (result == 0) {
result = config_parse(values, cfg_file, r, level, depth+1);
r = git_array_get(cfg_file->readers, index);
reader = git_array_get(cfg_file->readers, reader_idx);
}
else if (result == GIT_ENOTFOUND) {
giterr_clear();
result = 0;
}
git_buf_free(&r->buffer);
if (result < 0)
break;
}
break;
}
}
git__free(current_section);
return result;
}
static int write_section(git_filebuf *file, const char *key)
{
int result;
const char *dot;
git_buf buf = GIT_BUF_INIT;
/* All of this just for [section "subsection"] */
dot = strchr(key, '.');
git_buf_putc(&buf, '[');
if (dot == NULL) {
git_buf_puts(&buf, key);
} else {
char *escaped;
git_buf_put(&buf, key, dot - key);
escaped = escape_value(dot + 1);
GITERR_CHECK_ALLOC(escaped);
git_buf_printf(&buf, " \"%s\"", escaped);
git__free(escaped);
}
git_buf_puts(&buf, "]\n");
if (git_buf_oom(&buf))
return -1;
result = git_filebuf_write(file, git_buf_cstr(&buf), buf.size);
git_buf_free(&buf);
return result;
}
static const char *quotes_for_value(const char *value)
{
const char *ptr;
if (value[0] == ' ' || value[0] == '\0')
return "\"";
for (ptr = value; *ptr; ++ptr) {
if (*ptr == ';' || *ptr == '#')
return "\"";
}
if (ptr[-1] == ' ')
return "\"";
return "";
}
/*
* This is pretty much the parsing, except we write out anything we don't have
*/
static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char* value)
{
int result, c;
int section_matches = 0, last_section_matched = 0, preg_replaced = 0, write_trailer = 0;
const char *pre_end = NULL, *post_start = NULL, *data_start, *write_start;
char *current_section = NULL, *section, *name, *ldot;
git_filebuf file = GIT_FILEBUF_INIT;
struct reader *reader = git_array_get(cfg->readers, 0);
/* We need to read in our own config file */
result = git_futils_readbuffer(&reader->buffer, cfg->file_path);
/* Initialise the reading position */
if (result == GIT_ENOTFOUND) {
reader->read_ptr = NULL;
reader->eof = 1;
data_start = NULL;
git_buf_clear(&reader->buffer);
} else if (result == 0) {
reader->read_ptr = reader->buffer.ptr;
reader->eof = 0;
data_start = reader->read_ptr;
} else {
return -1; /* OS error when reading the file */
}
write_start = data_start;
/* Lock the file */
if ((result = git_filebuf_open(
&file, cfg->file_path, 0, GIT_CONFIG_FILE_MODE)) < 0) {
git_buf_free(&reader->buffer);
return result;
}
skip_bom(reader);
ldot = strrchr(key, '.');
name = ldot + 1;
section = git__strndup(key, ldot - key);
while (!reader->eof) {
c = reader_peek(reader, SKIP_WHITESPACE);
if (c == '\n') { /* We've arrived at the end of the file */
break;
} else if (c == '[') { /* 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 = reader->read_ptr;
git__free(current_section);
current_section = NULL;
if (parse_section_header(reader, &current_section) < 0)
goto rewrite_fail;
/* Keep track of when it stops matching */
last_section_matched = section_matches;
section_matches = !strcmp(current_section, section);
}
else if (c == ';' || c == '#') {
reader_consume_line(reader);
}
else {
/*
* 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 the section doesn't match and we are trying to delete an entry
* (value == NULL), we must continue searching; there may be another
* matching section later.
*/
if (!section_matches) {
if (!last_section_matched || value == NULL) {
reader_consume_line(reader);
continue;
}
} else {
int has_matched = 0;
char *var_name, *var_value;
pre_end = reader->read_ptr;
if (parse_variable(reader, &var_name, &var_value) < 0)
goto rewrite_fail;
/* First try to match the name of the variable */
if (strcasecmp(name, var_name) == 0)
has_matched = 1;
/* If the name matches, and we have a regex to match the
* value, try to match it */
if (has_matched && preg != NULL)
has_matched = (regexec(preg, var_value, 0, NULL, 0) == 0);
git__free(var_name);
git__free(var_value);
/* if there is no match, keep going */
if (!has_matched)
continue;
post_start = reader->read_ptr;
}
/* We've found the variable we wanted to change, so
* write anything up to it */
git_filebuf_write(&file, write_start, pre_end - write_start);
preg_replaced = 1;
/* Then replace the variable. If the value is NULL, it
* means we want to delete it, so don't write anything. */
if (value != NULL) {
const char *q = quotes_for_value(value);
git_filebuf_printf(&file, "\t%s = %s%s%s\n", name, q, value, q);
}
/*
* If we have a multivar, we should keep looking for entries,
* but only if we're in the right section. Otherwise we'll end up
* looping on the edge of a matching and a non-matching section.
*/
if (section_matches && preg != NULL) {
write_start = post_start;
continue;
}
write_trailer = 1;
break; /* break from the loop */
}
}
/*
* 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.
*
* 3) we're setting a multivar with a regex, which means we
* continue to search for matching values
*
* In the last case, if we've already replaced a value, we
* want to write the rest of the file. Otherwise we need to write
* out the whole file and then the new variable.
*/
if (write_trailer) {
/* Write out rest of the file */
git_filebuf_write(&file, post_start, reader->buffer.size - (post_start - data_start));
} else {
if (preg_replaced) {
git_filebuf_printf(&file, "\n%s", write_start);
} else {
const char *q;
git_filebuf_write(&file, reader->buffer.ptr, reader->buffer.size);
if (reader->buffer.size > 0 && *(reader->buffer.ptr + reader->buffer.size - 1) != '\n')
git_filebuf_write(&file, "\n", 1);
/* And now if we just need to add a variable */
if (!section_matches && write_section(&file, section) < 0)
goto rewrite_fail;
/* Sanity check: if we are here, and value is NULL, that means that somebody
* touched the config file after our initial read. We should probably assert()
* this, but instead we'll handle it gracefully with an error. */
if (value == NULL) {
giterr_set(GITERR_CONFIG,
"race condition when writing a config file (a cvar has been removed)");
goto rewrite_fail;
}
/* If we are here, there is at least a section line */
q = quotes_for_value(value);
git_filebuf_printf(&file, "\t%s = %s%s%s\n", name, q, value, q);
}
}
git__free(section);
git__free(current_section);
/* refresh stats - if this errors, then commit will error too */
(void)git_filebuf_stats(&reader->file_mtime, &reader->file_size, &file);
result = git_filebuf_commit(&file);
git_buf_free(&reader->buffer);
return result;
rewrite_fail:
git__free(section);
git__free(current_section);
git_filebuf_cleanup(&file);
git_buf_free(&reader->buffer);
return -1;
}
static const char *escapes = "ntb\"\\"; static const char *escapes = "ntb\"\\";
static const char *escaped = "\n\t\b\"\\"; static const char *escaped = "\n\t\b\"\\";
...@@ -1813,3 +1419,435 @@ on_error: ...@@ -1813,3 +1419,435 @@ on_error:
git__free(line); git__free(line);
return -1; return -1;
} }
static int config_parse(
struct reader *reader,
int (*on_section)(struct reader **reader, const char *current_section, const char *line, size_t line_len, void *data),
int (*on_variable)(struct reader **reader, const char *current_section, char *var_name, char *var_value, const char *line, size_t line_len, void *data),
int (*on_comment)(struct reader **reader, const char *line, size_t line_len, void *data),
int (*on_eof)(struct reader **reader, void *data),
void *data)
{
char *current_section = NULL, *var_name, *var_value, *line_start;
char c;
size_t line_len;
int result = 0;
skip_bom(reader);
while (result == 0 && !reader->eof) {
line_start = reader->read_ptr;
c = reader_peek(reader, SKIP_WHITESPACE);
switch (c) {
case '\0': /* EOF when peeking, set EOF in the reader to exit the loop */
reader->eof = 1;
break;
case '[': /* section header, new section begins */
git__free(current_section);
current_section = NULL;
if ((result = parse_section_header(reader, &current_section)) == 0 && on_section) {
line_len = reader->read_ptr - line_start;
result = on_section(&reader, current_section, line_start, line_len, data);
}
break;
case '\n': /* comment or whitespace-only */
case ';':
case '#':
reader_consume_line(reader);
if (on_comment) {
line_len = reader->read_ptr - line_start;
result = on_comment(&reader, line_start, line_len, data);
}
break;
default: /* assume variable declaration */
if ((result = parse_variable(reader, &var_name, &var_value)) == 0 && on_variable) {
line_len = reader->read_ptr - line_start;
result = on_variable(&reader, current_section, var_name, var_value, line_start, line_len, data);
}
break;
}
}
if (on_eof)
result = on_eof(&reader, data);
git__free(current_section);
return result;
}
struct parse_data {
git_strmap *values;
diskfile_backend *cfg_file;
uint32_t reader_idx;
git_config_level_t level;
int depth;
};
static int read_on_variable(
struct reader **reader,
const char *current_section,
char *var_name,
char *var_value,
const char *line,
size_t line_len,
void *data)
{
struct parse_data *parse_data = (struct parse_data *)data;
git_buf buf = GIT_BUF_INIT;
cvar_t *var;
int result = 0;
GIT_UNUSED(line);
GIT_UNUSED(line_len);
git__strtolower(var_name);
git_buf_printf(&buf, "%s.%s", current_section, var_name);
git__free(var_name);
if (git_buf_oom(&buf)) {
git__free(var_value);
return -1;
}
var = git__calloc(1, sizeof(cvar_t));
GITERR_CHECK_ALLOC(var);
var->entry = git__calloc(1, sizeof(git_config_entry));
GITERR_CHECK_ALLOC(var->entry);
var->entry->name = git_buf_detach(&buf);
var->entry->value = var_value;
var->entry->level = parse_data->level;
var->included = !!parse_data->depth;
if ((result = append_entry(parse_data->values, var)) < 0)
return result;
result = 0;
/* Add or append the new config option */
if (!git__strcmp(var->entry->name, "include.path")) {
struct reader *r;
git_buf path = GIT_BUF_INIT;
char *dir;
uint32_t index;
r = git_array_alloc(parse_data->cfg_file->readers);
/* The reader may have been reallocated */
*reader = git_array_get(parse_data->cfg_file->readers, parse_data->reader_idx);
memset(r, 0, sizeof(struct reader));
if ((result = git_path_dirname_r(&path, (*reader)->file_path)) < 0)
return result;
/* We need to know our index in the array, as the next config_parse call may realloc */
index = git_array_size(parse_data->cfg_file->readers) - 1;
dir = git_buf_detach(&path);
result = included_path(&path, dir, var->entry->value);
git__free(dir);
if (result < 0)
return result;
r->file_path = git_buf_detach(&path);
git_buf_init(&r->buffer, 0);
result = git_futils_readbuffer_updated(
&r->buffer, r->file_path, &r->file_mtime, &r->file_size, NULL);
if (result == 0) {
result = config_read(parse_data->values, parse_data->cfg_file, r, parse_data->level, parse_data->depth+1);
r = git_array_get(parse_data->cfg_file->readers, index);
*reader = git_array_get(parse_data->cfg_file->readers, parse_data->reader_idx);
} else if (result == GIT_ENOTFOUND) {
giterr_clear();
result = 0;
}
git_buf_free(&r->buffer);
}
return result;
}
static int config_read(git_strmap *values, diskfile_backend *cfg_file, struct reader *reader, git_config_level_t level, int depth)
{
struct parse_data parse_data;
if (depth >= MAX_INCLUDE_DEPTH) {
giterr_set(GITERR_CONFIG, "Maximum config include depth reached");
return -1;
}
/* Initialize the reading position */
reader->read_ptr = reader->buffer.ptr;
reader->eof = 0;
/* If the file is empty, there's nothing for us to do */
if (*reader->read_ptr == '\0')
return 0;
parse_data.values = values;
parse_data.cfg_file = cfg_file;
parse_data.reader_idx = git_array_size(cfg_file->readers) - 1;
parse_data.level = level;
parse_data.depth = depth;
return config_parse(reader, NULL, read_on_variable, NULL, NULL, &parse_data);
}
static int write_section(git_filebuf *file, const char *key)
{
int result;
const char *dot;
git_buf buf = GIT_BUF_INIT;
/* All of this just for [section "subsection"] */
dot = strchr(key, '.');
git_buf_putc(&buf, '[');
if (dot == NULL) {
git_buf_puts(&buf, key);
} else {
char *escaped;
git_buf_put(&buf, key, dot - key);
escaped = escape_value(dot + 1);
GITERR_CHECK_ALLOC(escaped);
git_buf_printf(&buf, " \"%s\"", escaped);
git__free(escaped);
}
git_buf_puts(&buf, "]\n");
if (git_buf_oom(&buf))
return -1;
result = git_filebuf_write(file, git_buf_cstr(&buf), buf.size);
git_buf_free(&buf);
return result;
}
static const char *quotes_for_value(const char *value)
{
const char *ptr;
if (value[0] == ' ' || value[0] == '\0')
return "\"";
for (ptr = value; *ptr; ++ptr) {
if (*ptr == ';' || *ptr == '#')
return "\"";
}
if (ptr[-1] == ' ')
return "\"";
return "";
}
struct write_data {
git_filebuf *file;
unsigned int in_section : 1,
preg_replaced : 1;
const char *section;
const char *name;
const regex_t *preg;
const char *value;
};
static int write_line(struct write_data *write_data, const char *line, size_t line_len)
{
int result = git_filebuf_write(write_data->file, line, line_len);
if (!result && line_len && line[line_len-1] != '\n')
result = git_filebuf_printf(write_data->file, "\n");
return result;
}
static int write_value(struct write_data *write_data)
{
const char *q;
int result;
q = quotes_for_value(write_data->value);
result = git_filebuf_printf(write_data->file,
"\t%s = %s%s%s\n", write_data->name, q, write_data->value, q);
/* If we are updating a single name/value, we're done. Setting `value`
* to `NULL` will prevent us from trying to write it again later (in
* `write_on_section`) if we see the same section repeated.
*/
if (!write_data->preg)
write_data->value = NULL;
return result;
}
static int write_on_section(
struct reader **reader,
const char *current_section,
const char *line,
size_t line_len,
void *data)
{
struct write_data *write_data = (struct write_data *)data;
int result = 0;
GIT_UNUSED(reader);
/* If we were previously in the correct section (but aren't anymore)
* and haven't written our value (for a simple name/value set, not
* a multivar), then append it to the end of the section before writing
* the new one.
*/
if (write_data->in_section && !write_data->preg && write_data->value)
result = write_value(write_data);
write_data->in_section = strcmp(current_section, write_data->section) == 0;
if (!result)
result = write_line(write_data, line, line_len);
return result;
}
static int write_on_variable(
struct reader **reader,
const char *current_section,
char *var_name,
char *var_value,
const char *line,
size_t line_len,
void *data)
{
struct write_data *write_data = (struct write_data *)data;
bool has_matched = false;
GIT_UNUSED(reader);
GIT_UNUSED(current_section);
/* See if we are to update this name/value pair; first examine name */
if (write_data->in_section &&
strcasecmp(write_data->name, var_name) == 0)
has_matched = true;
/* If we have a regex to match the value, see if it matches */
if (has_matched && write_data->preg != NULL)
has_matched = (regexec(write_data->preg, var_value, 0, NULL, 0) == 0);
git__free(var_name);
git__free(var_value);
/* If this isn't the name/value we're looking for, simply dump the
* existing data back out and continue on.
*/
if (!has_matched)
return write_line(write_data, line, line_len);
write_data->preg_replaced = 1;
/* If value is NULL, we are deleting this value; write nothing. */
if (!write_data->value)
return 0;
return write_value(write_data);
}
static int write_on_comment(struct reader **reader, const char *line, size_t line_len, void *data)
{
struct write_data *write_data;
GIT_UNUSED(reader);
write_data = (struct write_data *)data;
return write_line(write_data, line, line_len);
}
static int write_on_eof(struct reader **reader, void *data)
{
struct write_data *write_data = (struct write_data *)data;
int result = 0;
GIT_UNUSED(reader);
/* If we are at the EOF and have not written our value (again, for a
* simple name/value set, not a multivar) then we have never seen the
* section in question and should create a new section and write the
* value.
*/
if ((!write_data->preg || !write_data->preg_replaced) && write_data->value) {
if ((result = write_section(write_data->file, write_data->section)) == 0)
result = write_value(write_data);
}
return result;
}
/*
* This is pretty much the parsing, except we write out anything we don't have
*/
static int config_write(diskfile_backend *cfg, const char *key, const regex_t *preg, const char* value)
{
int result;
char *section, *name, *ldot;
git_filebuf file = GIT_FILEBUF_INIT;
struct reader *reader = git_array_get(cfg->readers, 0);
struct write_data write_data;
/* Lock the file */
if ((result = git_filebuf_open(
&file, cfg->file_path, 0, GIT_CONFIG_FILE_MODE)) < 0) {
git_buf_free(&reader->buffer);
return result;
}
/* We need to read in our own config file */
result = git_futils_readbuffer(&reader->buffer, cfg->file_path);
/* Initialise the reading position */
if (result == GIT_ENOTFOUND) {
reader->read_ptr = NULL;
reader->eof = 1;
git_buf_clear(&reader->buffer);
} else if (result == 0) {
reader->read_ptr = reader->buffer.ptr;
reader->eof = 0;
} else {
git_filebuf_cleanup(&file);
return -1; /* OS error when reading the file */
}
ldot = strrchr(key, '.');
name = ldot + 1;
section = git__strndup(key, ldot - key);
write_data.file = &file;
write_data.section = section;
write_data.in_section = 0;
write_data.preg_replaced = 0;
write_data.name = name;
write_data.preg = preg;
write_data.value = value;
if ((result = config_parse(reader, write_on_section, write_on_variable, write_on_comment, write_on_eof, &write_data)) < 0) {
git_filebuf_cleanup(&file);
goto done;
}
/* refresh stats - if this errors, then commit will error too */
(void)git_filebuf_stats(&reader->file_mtime, &reader->file_size, &file);
result = git_filebuf_commit(&file);
git_buf_free(&reader->buffer);
done:
git_buf_free(&reader->buffer);
return result;
}
...@@ -145,6 +145,138 @@ void test_config_write__delete_value_with_duplicate_header(void) ...@@ -145,6 +145,138 @@ void test_config_write__delete_value_with_duplicate_header(void)
git_config_free(cfg); git_config_free(cfg);
} }
/*
* This test exposes a bug where duplicate section headers could cause
* config_write to add a new entry when one already exists.
*/
void test_config_write__add_value_with_duplicate_header(void)
{
const char *file_name = "config-duplicate-insert";
const char *entry_name = "foo.c";
const char *old_val = "old";
const char *new_val = "new";
const char *str;
git_config *cfg, *snapshot;
/* c = old should be replaced by c = new.
* The bug causes c = new to be inserted under the first 'foo' header.
*/
const char *file_content =
"[foo]\n" \
" a = b\n" \
"[other]\n" \
" a = b\n" \
"[foo]\n" \
" c = old\n";
/* Write the test config */
cl_git_mkfile(file_name, file_content);
cl_git_pass(git_config_open_ondisk(&cfg, file_name));
/* make sure the expected entry (foo.c) exists */
cl_git_pass(git_config_snapshot(&snapshot, cfg));
cl_git_pass(git_config_get_string(&str, snapshot, entry_name));
cl_assert_equal_s(old_val, str);
git_config_free(snapshot);
/* Try setting foo.c to something else */
cl_git_pass(git_config_set_string(cfg, entry_name, new_val));
git_config_free(cfg);
/* Reopen the file and make sure the new value was set */
cl_git_pass(git_config_open_ondisk(&cfg, file_name));
cl_git_pass(git_config_snapshot(&snapshot, cfg));
cl_git_pass(git_config_get_string(&str, snapshot, entry_name));
cl_assert_equal_s(new_val, str);
/* Cleanup */
git_config_free(snapshot);
git_config_free(cfg);
}
void test_config_write__overwrite_value_with_duplicate_header(void)
{
const char *file_name = "config-duplicate-header";
const char *entry_name = "remote.origin.url";
git_config *cfg;
git_config_entry *entry;
/* This config can occur after removing and re-adding the origin remote */
const char *file_content =
"[remote \"origin\"]\n" \
"[branch \"master\"]\n" \
" remote = \"origin\"\n" \
"[remote \"origin\"]\n" \
" url = \"foo\"\n";
/* Write the test config and make sure the expected entry exists */
cl_git_mkfile(file_name, file_content);
cl_git_pass(git_config_open_ondisk(&cfg, file_name));
cl_git_pass(git_config_get_entry(&entry, cfg, entry_name));
/* Update that entry */
cl_git_pass(git_config_set_string(cfg, entry_name, "newurl"));
/* Reopen the file and make sure the entry was updated */
git_config_entry_free(entry);
git_config_free(cfg);
cl_git_pass(git_config_open_ondisk(&cfg, file_name));
cl_git_pass(git_config_get_entry(&entry, cfg, entry_name));
cl_assert_equal_s("newurl", entry->value);
/* Cleanup */
git_config_entry_free(entry);
git_config_free(cfg);
}
static int multivar_cb(const git_config_entry *entry, void *data)
{
int *n = (int *)data;
cl_assert_equal_s(entry->value, "newurl");
(*n)++;
return 0;
}
void test_config_write__overwrite_multivar_within_duplicate_header(void)
{
const char *file_name = "config-duplicate-header";
const char *entry_name = "remote.origin.url";
git_config *cfg;
git_config_entry *entry;
int n = 0;
/* This config can occur after removing and re-adding the origin remote */
const char *file_content =
"[remote \"origin\"]\n" \
" url = \"bar\"\n" \
"[branch \"master\"]\n" \
" remote = \"origin\"\n" \
"[remote \"origin\"]\n" \
" url = \"foo\"\n";
/* Write the test config and make sure the expected entry exists */
cl_git_mkfile(file_name, file_content);
cl_git_pass(git_config_open_ondisk(&cfg, file_name));
cl_git_pass(git_config_get_entry(&entry, cfg, entry_name));
/* Update that entry */
cl_git_pass(git_config_set_multivar(cfg, entry_name, ".*", "newurl"));
git_config_entry_free(entry);
git_config_free(cfg);
/* Reopen the file and make sure the entry was updated */
cl_git_pass(git_config_open_ondisk(&cfg, file_name));
cl_git_pass(git_config_get_multivar_foreach(cfg, entry_name, NULL, multivar_cb, &n));
cl_assert_equal_i(2, n);
/* Cleanup */
git_config_free(cfg);
}
void test_config_write__write_subsection(void) void test_config_write__write_subsection(void)
{ {
git_config *cfg; git_config *cfg;
...@@ -395,6 +527,75 @@ void test_config_write__outside_change(void) ...@@ -395,6 +527,75 @@ void test_config_write__outside_change(void)
git_config_free(cfg); git_config_free(cfg);
} }
#define SECTION_FOO \
"\n" \
" \n" \
" [section \"foo\"] \n" \
" # here's a comment\n" \
"\tname = \"value\"\n" \
" name2 = \"value2\"\n" \
"; another comment!\n"
#define SECTION_BAR \
"[section \"bar\"]\t\n" \
"\t \n" \
" barname=\"value\"\n"
void test_config_write__preserves_whitespace_and_comments(void)
{
const char *file_name = "config-duplicate-header";
const char *n;
git_config *cfg;
git_buf newfile = GIT_BUF_INIT;
/* This config can occur after removing and re-adding the origin remote */
const char *file_content = SECTION_FOO SECTION_BAR;
/* Write the test config and make sure the expected entry exists */
cl_git_mkfile(file_name, file_content);
cl_git_pass(git_config_open_ondisk(&cfg, file_name));
cl_git_pass(git_config_set_string(cfg, "section.foo.other", "otherval"));
cl_git_pass(git_config_set_string(cfg, "newsection.newname", "new_value"));
/* Ensure that we didn't needlessly mangle the config file */
cl_git_pass(git_futils_readbuffer(&newfile, file_name));
n = newfile.ptr;
cl_assert_equal_strn(SECTION_FOO, n, strlen(SECTION_FOO));
n += strlen(SECTION_FOO);
cl_assert_equal_strn("\tother = otherval\n", n, strlen("\tother = otherval\n"));
n += strlen("\tother = otherval\n");
cl_assert_equal_strn(SECTION_BAR, n, strlen(SECTION_BAR));
n += strlen(SECTION_BAR);
cl_assert_equal_s("[newsection]\n\tnewname = new_value\n", n);
git_buf_free(&newfile);
git_config_free(cfg);
}
void test_config_write__preserves_entry_with_name_only(void)
{
const char *file_name = "config-empty-value";
git_config *cfg;
git_buf newfile = GIT_BUF_INIT;
/* Write the test config and make sure the expected entry exists */
cl_git_mkfile(file_name, "[section \"foo\"]\n\tname\n");
cl_git_pass(git_config_open_ondisk(&cfg, file_name));
cl_git_pass(git_config_set_string(cfg, "newsection.newname", "new_value"));
cl_git_pass(git_config_set_string(cfg, "section.foo.other", "otherval"));
cl_git_pass(git_futils_readbuffer(&newfile, file_name));
cl_assert_equal_s("[section \"foo\"]\n\tname\n\tother = otherval\n[newsection]\n\tnewname = new_value\n", newfile.ptr);
git_buf_free(&newfile);
git_config_free(cfg);
}
void test_config_write__to_empty_file(void) void test_config_write__to_empty_file(void)
{ {
git_config *cfg; git_config *cfg;
...@@ -428,3 +629,4 @@ void test_config_write__to_file_with_only_comment(void) ...@@ -428,3 +629,4 @@ void test_config_write__to_file_with_only_comment(void)
git_buf_free(&result); git_buf_free(&result);
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment