}
}
-static int git_parse_source(config_fn_t fn, void *data)
+struct parse_event_data {
+ enum config_event_t previous_type;
+ size_t previous_offset;
+ const struct config_options *opts;
+};
+
+static int do_event(enum config_event_t type, struct parse_event_data *data)
+{
+ size_t offset;
+
+ if (!data->opts || !data->opts->event_fn)
+ return 0;
+
+ if (type == CONFIG_EVENT_WHITESPACE &&
+ data->previous_type == type)
+ return 0;
+
+ offset = cf->do_ftell(cf);
+ /*
+ * At EOF, the parser always "inserts" an extra '\n', therefore
+ * the end offset of the event is the current file position, otherwise
+ * we will already have advanced to the next event.
+ */
+ if (type != CONFIG_EVENT_EOF)
+ offset--;
+
+ if (data->previous_type != CONFIG_EVENT_EOF &&
+ data->opts->event_fn(data->previous_type, data->previous_offset,
+ offset, data->opts->event_fn_data) < 0)
+ return -1;
+
+ data->previous_type = type;
+ data->previous_offset = offset;
+
+ return 0;
+}
+
+static int git_parse_source(config_fn_t fn, void *data,
+ const struct config_options *opts)
{
int comment = 0;
int baselen = 0;
/* U+FEFF Byte Order Mark in UTF8 */
const char *bomptr = utf8_bom;
+ /* For the parser event callback */
+ struct parse_event_data event_data = {
+ CONFIG_EVENT_EOF, 0, opts
+ };
+
for (;;) {
- int c = get_next_char();
+ int c;
+
+ c = get_next_char();
if (bomptr && *bomptr) {
/* We are at the file beginning; skip UTF8-encoded BOM
* if present. Sane editors won't put this in on their
}
}
if (c == '\n') {
- if (cf->eof)
+ if (cf->eof) {
+ if (do_event(CONFIG_EVENT_EOF, &event_data) < 0)
+ return -1;
return 0;
+ }
+ if (do_event(CONFIG_EVENT_WHITESPACE, &event_data) < 0)
+ return -1;
comment = 0;
continue;
}
- if (comment || isspace(c))
+ if (comment)
+ continue;
+ if (isspace(c)) {
+ if (do_event(CONFIG_EVENT_WHITESPACE, &event_data) < 0)
+ return -1;
continue;
+ }
if (c == '#' || c == ';') {
+ if (do_event(CONFIG_EVENT_COMMENT, &event_data) < 0)
+ return -1;
comment = 1;
continue;
}
if (c == '[') {
+ if (do_event(CONFIG_EVENT_SECTION, &event_data) < 0)
+ return -1;
+
/* Reset prior to determining a new stem */
strbuf_reset(var);
if (get_base_var(var) < 0 || var->len < 1)
}
if (!isalpha(c))
break;
+
+ if (do_event(CONFIG_EVENT_ENTRY, &event_data) < 0)
+ return -1;
+
/*
* Truncate the var name back to the section header
* stem prior to grabbing the suffix part of the name
break;
}
+ if (do_event(CONFIG_EVENT_ERROR, &event_data) < 0)
+ return -1;
+
switch (cf->origin_type) {
case CONFIG_ORIGIN_BLOB:
error_msg = xstrfmt(_("bad config line %d in blob %s"),
* fgetc, ungetc, ftell of top need to be initialized before calling
* this function.
*/
-static int do_config_from(struct config_source *top, config_fn_t fn, void *data)
+static int do_config_from(struct config_source *top, config_fn_t fn, void *data,
+ const struct config_options *opts)
{
int ret;
strbuf_init(&top->var, 1024);
cf = top;
- ret = git_parse_source(fn, data);
+ ret = git_parse_source(fn, data, opts);
/* pop config-file parsing state stack */
strbuf_release(&top->value);
static int do_config_from_file(config_fn_t fn,
const enum config_origin_type origin_type,
const char *name, const char *path, FILE *f,
- void *data)
+ void *data, const struct config_options *opts)
{
struct config_source top;
int ret;
top.do_ftell = config_file_ftell;
flockfile(f);
- ret = do_config_from(&top, fn, data);
+ ret = do_config_from(&top, fn, data, opts);
funlockfile(f);
return ret;
}
static int git_config_from_stdin(config_fn_t fn, void *data)
{
- return do_config_from_file(fn, CONFIG_ORIGIN_STDIN, "", NULL, stdin, data);
+ return do_config_from_file(fn, CONFIG_ORIGIN_STDIN, "", NULL, stdin,
+ data, NULL);
}
-int git_config_from_file(config_fn_t fn, const char *filename, void *data)
+int git_config_from_file_with_options(config_fn_t fn, const char *filename,
+ void *data,
+ const struct config_options *opts)
{
int ret = -1;
FILE *f;
f = fopen_or_warn(filename, "r");
if (f) {
- ret = do_config_from_file(fn, CONFIG_ORIGIN_FILE, filename, filename, f, data);
+ ret = do_config_from_file(fn, CONFIG_ORIGIN_FILE, filename,
+ filename, f, data, opts);
fclose(f);
}
return ret;
}
+int git_config_from_file(config_fn_t fn, const char *filename, void *data)
+{
+ return git_config_from_file_with_options(fn, filename, data, NULL);
+}
+
int git_config_from_mem(config_fn_t fn, const enum config_origin_type origin_type,
const char *name, const char *buf, size_t len, void *data)
{
top.do_ungetc = config_buf_ungetc;
top.do_ftell = config_buf_ftell;
- return do_config_from(&top, fn, data);
+ return do_config_from(&top, fn, data, NULL);
}
int git_config_from_blob_oid(config_fn_t fn,
* Find all the stuff for git_config_set() below.
*/
-static struct {
+struct config_store_data {
int baselen;
char *key;
int do_not_match;
regex_t *value_regex;
int multi_replace;
- size_t *offset;
- unsigned int offset_alloc;
- enum { START, SECTION_SEEN, SECTION_END_SEEN, KEY_SEEN } state;
- unsigned int seen;
-} store;
+ struct {
+ size_t begin, end;
+ enum config_event_t type;
+ int is_keys_section;
+ } *parsed;
+ unsigned int parsed_nr, parsed_alloc, *seen, seen_nr, seen_alloc;
+ unsigned int key_seen:1, section_seen:1, is_keys_section:1;
+};
-static int matches(const char *key, const char *value)
+static int matches(const char *key, const char *value,
+ const struct config_store_data *store)
{
- if (strcmp(key, store.key))
+ if (strcmp(key, store->key))
return 0; /* not ours */
- if (!store.value_regex)
+ if (!store->value_regex)
return 1; /* always matches */
- if (store.value_regex == CONFIG_REGEX_NONE)
+ if (store->value_regex == CONFIG_REGEX_NONE)
return 0; /* never matches */
- return store.do_not_match ^
- (value && !regexec(store.value_regex, value, 0, NULL, 0));
+ return store->do_not_match ^
+ (value && !regexec(store->value_regex, value, 0, NULL, 0));
+}
+
+static int store_aux_event(enum config_event_t type,
+ size_t begin, size_t end, void *data)
+{
+ struct config_store_data *store = data;
+
+ ALLOC_GROW(store->parsed, store->parsed_nr + 1, store->parsed_alloc);
+ store->parsed[store->parsed_nr].begin = begin;
+ store->parsed[store->parsed_nr].end = end;
+ store->parsed[store->parsed_nr].type = type;
+
+ if (type == CONFIG_EVENT_SECTION) {
+ if (cf->var.len < 2 || cf->var.buf[cf->var.len - 1] != '.')
+ BUG("Invalid section name '%s'", cf->var.buf);
+
+ /* Is this the section we were looking for? */
+ store->is_keys_section =
+ store->parsed[store->parsed_nr].is_keys_section =
+ cf->var.len - 1 == store->baselen &&
+ !strncasecmp(cf->var.buf, store->key, store->baselen);
+ if (store->is_keys_section) {
+ store->section_seen = 1;
+ ALLOC_GROW(store->seen, store->seen_nr + 1,
+ store->seen_alloc);
+ store->seen[store->seen_nr] = store->parsed_nr;
+ }
+ }
+
+ store->parsed_nr++;
+
+ return 0;
}
static int store_aux(const char *key, const char *value, void *cb)
{
- const char *ep;
- size_t section_len;
+ struct config_store_data *store = cb;
- switch (store.state) {
- case KEY_SEEN:
- if (matches(key, value)) {
- if (store.seen == 1 && store.multi_replace == 0) {
+ if (store->key_seen) {
+ if (matches(key, value, store)) {
+ if (store->seen_nr == 1 && store->multi_replace == 0) {
warning(_("%s has multiple values"), key);
}
- ALLOC_GROW(store.offset, store.seen + 1,
- store.offset_alloc);
+ ALLOC_GROW(store->seen, store->seen_nr + 1,
+ store->seen_alloc);
- store.offset[store.seen] = cf->do_ftell(cf);
- store.seen++;
+ store->seen[store->seen_nr] = store->parsed_nr;
+ store->seen_nr++;
}
- break;
- case SECTION_SEEN:
+ } else if (store->is_keys_section) {
/*
- * What we are looking for is in store.key (both
- * section and var), and its section part is baselen
- * long. We found key (again, both section and var).
- * We would want to know if this key is in the same
- * section as what we are looking for. We already
- * know we are in the same section as what should
- * hold store.key.
+ * Do not increment matches yet: this may not be a match, but we
+ * are in the desired section.
*/
- ep = strrchr(key, '.');
- section_len = ep - key;
+ ALLOC_GROW(store->seen, store->seen_nr + 1, store->seen_alloc);
+ store->seen[store->seen_nr] = store->parsed_nr;
+ store->section_seen = 1;
- if ((section_len != store.baselen) ||
- memcmp(key, store.key, section_len+1)) {
- store.state = SECTION_END_SEEN;
- break;
- }
-
- /*
- * Do not increment matches: this is no match, but we
- * just made sure we are in the desired section.
- */
- ALLOC_GROW(store.offset, store.seen + 1,
- store.offset_alloc);
- store.offset[store.seen] = cf->do_ftell(cf);
- /* fallthru */
- case SECTION_END_SEEN:
- case START:
- if (matches(key, value)) {
- ALLOC_GROW(store.offset, store.seen + 1,
- store.offset_alloc);
- store.offset[store.seen] = cf->do_ftell(cf);
- store.state = KEY_SEEN;
- store.seen++;
- } else {
- if (strrchr(key, '.') - key == store.baselen &&
- !strncmp(key, store.key, store.baselen)) {
- store.state = SECTION_SEEN;
- ALLOC_GROW(store.offset,
- store.seen + 1,
- store.offset_alloc);
- store.offset[store.seen] = cf->do_ftell(cf);
- }
+ if (matches(key, value, store)) {
+ store->seen_nr++;
+ store->key_seen = 1;
}
}
+
return 0;
}
return 4;
}
-static struct strbuf store_create_section(const char *key)
+static struct strbuf store_create_section(const char *key,
+ const struct config_store_data *store)
{
const char *dot;
int i;
struct strbuf sb = STRBUF_INIT;
- dot = memchr(key, '.', store.baselen);
+ dot = memchr(key, '.', store->baselen);
if (dot) {
strbuf_addf(&sb, "[%.*s \"", (int)(dot - key), key);
- for (i = dot - key + 1; i < store.baselen; i++) {
+ for (i = dot - key + 1; i < store->baselen; i++) {
if (key[i] == '"' || key[i] == '\\')
strbuf_addch(&sb, '\\');
strbuf_addch(&sb, key[i]);
}
strbuf_addstr(&sb, "\"]\n");
} else {
- strbuf_addf(&sb, "[%.*s]\n", store.baselen, key);
+ strbuf_addf(&sb, "[%.*s]\n", store->baselen, key);
}
return sb;
}
-static ssize_t write_section(int fd, const char *key)
+static ssize_t write_section(int fd, const char *key,
+ const struct config_store_data *store)
{
- struct strbuf sb = store_create_section(key);
+ struct strbuf sb = store_create_section(key, store);
ssize_t ret;
ret = write_in_full(fd, sb.buf, sb.len);
return ret;
}
-static ssize_t write_pair(int fd, const char *key, const char *value)
+static ssize_t write_pair(int fd, const char *key, const char *value,
+ const struct config_store_data *store)
{
int i;
ssize_t ret;
- int length = strlen(key + store.baselen + 1);
+ int length = strlen(key + store->baselen + 1);
const char *quote = "";
struct strbuf sb = STRBUF_INIT;
quote = "\"";
strbuf_addf(&sb, "\t%.*s = %s",
- length, key + store.baselen + 1, quote);
+ length, key + store->baselen + 1, quote);
for (i = 0; value[i]; i++)
switch (value[i]) {
return ret;
}
-static ssize_t find_beginning_of_line(const char *contents, size_t size,
- size_t offset_, int *found_bracket)
+/*
+ * If we are about to unset the last key(s) in a section, and if there are
+ * no comments surrounding (or included in) the section, we will want to
+ * extend begin/end to remove the entire section.
+ *
+ * Note: the parameter `seen_ptr` points to the index into the store.seen
+ * array. * This index may be incremented if a section has more than one
+ * entry (which all are to be removed).
+ */
+static void maybe_remove_section(struct config_store_data *store,
+ const char *contents,
+ size_t *begin_offset, size_t *end_offset,
+ int *seen_ptr)
{
- size_t equal_offset = size, bracket_offset = size;
- ssize_t offset;
+ size_t begin;
+ int i, seen, section_seen = 0;
-contline:
- for (offset = offset_-2; offset > 0
- && contents[offset] != '\n'; offset--)
- switch (contents[offset]) {
- case '=': equal_offset = offset; break;
- case ']': bracket_offset = offset; break;
+ /*
+ * First, ensure that this is the first key, and that there are no
+ * comments before the entry nor before the section header.
+ */
+ seen = *seen_ptr;
+ for (i = store->seen[seen]; i > 0; i--) {
+ enum config_event_t type = store->parsed[i - 1].type;
+
+ if (type == CONFIG_EVENT_COMMENT)
+ /* There is a comment before this entry or section */
+ return;
+ if (type == CONFIG_EVENT_ENTRY) {
+ if (!section_seen)
+ /* This is not the section's first entry. */
+ return;
+ /* We encountered no comment before the section. */
+ break;
+ }
+ if (type == CONFIG_EVENT_SECTION) {
+ if (!store->parsed[i - 1].is_keys_section)
+ break;
+ section_seen = 1;
}
- if (offset > 0 && contents[offset-1] == '\\') {
- offset_ = offset;
- goto contline;
}
- if (bracket_offset < equal_offset) {
- *found_bracket = 1;
- offset = bracket_offset+1;
- } else
- offset++;
+ begin = store->parsed[i].begin;
+
+ /*
+ * Next, make sure that we are removing he last key(s) in the section,
+ * and that there are no comments that are possibly about the current
+ * section.
+ */
+ for (i = store->seen[seen] + 1; i < store->parsed_nr; i++) {
+ enum config_event_t type = store->parsed[i].type;
- return offset;
+ if (type == CONFIG_EVENT_COMMENT)
+ return;
+ if (type == CONFIG_EVENT_SECTION) {
+ if (store->parsed[i].is_keys_section)
+ continue;
+ break;
+ }
+ if (type == CONFIG_EVENT_ENTRY) {
+ if (++seen < store->seen_nr &&
+ i == store->seen[seen])
+ /* We want to remove this entry, too */
+ continue;
+ /* There is another entry in this section. */
+ return;
+ }
+ }
+
+ /*
+ * We are really removing the last entry/entries from this section, and
+ * there are no enclosed or surrounding comments. Remove the entire,
+ * now-empty section.
+ */
+ *seen_ptr = seen;
+ *begin_offset = begin;
+ if (i < store->parsed_nr)
+ *end_offset = store->parsed[i].begin;
+ else
+ *end_offset = store->parsed[store->parsed_nr - 1].end;
}
int git_config_set_in_file_gently(const char *config_filename,
char *filename_buf = NULL;
char *contents = NULL;
size_t contents_sz;
+ struct config_store_data store;
+
+ memset(&store, 0, sizeof(store));
/* parse-key returns negative; flip the sign to feed exit(3) */
ret = 0 - git_config_parse_key(key, &store.key, &store.baselen);
}
store.key = (char *)key;
- if (write_section(fd, key) < 0 ||
- write_pair(fd, key, value) < 0)
+ if (write_section(fd, key, &store) < 0 ||
+ write_pair(fd, key, value, &store) < 0)
goto write_err_out;
} else {
struct stat st;
size_t copy_begin, copy_end;
int i, new_line = 0;
+ struct config_options opts;
if (value_regex == NULL)
store.value_regex = NULL;
}
}
- ALLOC_GROW(store.offset, 1, store.offset_alloc);
- store.offset[0] = 0;
- store.state = START;
- store.seen = 0;
+ ALLOC_GROW(store.parsed, 1, store.parsed_alloc);
+ store.parsed[0].end = 0;
+
+ memset(&opts, 0, sizeof(opts));
+ opts.event_fn = store_aux_event;
+ opts.event_fn_data = &store;
/*
- * After this, store.offset will contain the *end* offset
- * of the last match, or remain at 0 if no match was found.
+ * After this, store.parsed will contain offsets of all the
+ * parsed elements, and store.seen will contain a list of
+ * matches, as indices into store.parsed.
+ *
* As a side effect, we make sure to transform only a valid
* existing config file.
*/
- if (git_config_from_file(store_aux, config_filename, NULL)) {
+ if (git_config_from_file_with_options(store_aux,
+ config_filename,
+ &store, &opts)) {
error("invalid config file %s", config_filename);
free(store.key);
if (store.value_regex != NULL &&
}
/* if nothing to unset, or too many matches, error out */
- if ((store.seen == 0 && value == NULL) ||
- (store.seen > 1 && multi_replace == 0)) {
+ if ((store.seen_nr == 0 && value == NULL) ||
+ (store.seen_nr > 1 && multi_replace == 0)) {
ret = CONFIG_NOTHING_SET;
goto out_free;
}
goto out_free;
}
- if (store.seen == 0)
- store.seen = 1;
+ if (store.seen_nr == 0) {
+ if (!store.seen_alloc) {
+ /* Did not see key nor section */
+ ALLOC_GROW(store.seen, 1, store.seen_alloc);
+ store.seen[0] = store.parsed_nr
+ - !!store.parsed_nr;
+ }
+ store.seen_nr = 1;
+ }
- for (i = 0, copy_begin = 0; i < store.seen; i++) {
- if (store.offset[i] == 0) {
- store.offset[i] = copy_end = contents_sz;
- } else if (store.state != KEY_SEEN) {
- copy_end = store.offset[i];
- } else
- copy_end = find_beginning_of_line(
- contents, contents_sz,
- store.offset[i]-2, &new_line);
+ for (i = 0, copy_begin = 0; i < store.seen_nr; i++) {
+ size_t replace_end;
+ int j = store.seen[i];
+
+ new_line = 0;
+ if (!store.key_seen) {
+ copy_end = store.parsed[j].end;
+ /* include '\n' when copying section header */
+ if (copy_end > 0 && copy_end < contents_sz &&
+ contents[copy_end - 1] != '\n' &&
+ contents[copy_end] == '\n')
+ copy_end++;
+ replace_end = copy_end;
+ } else {
+ replace_end = store.parsed[j].end;
+ copy_end = store.parsed[j].begin;
+ if (!value)
+ maybe_remove_section(&store, contents,
+ ©_end,
+ &replace_end, &i);
+ /*
+ * Swallow preceding white-space on the same
+ * line.
+ */
+ while (copy_end > 0 ) {
+ char c = contents[copy_end - 1];
+
+ if (isspace(c) && c != '\n')
+ copy_end--;
+ else
+ break;
+ }
+ }
if (copy_end > 0 && contents[copy_end-1] != '\n')
new_line = 1;
write_str_in_full(fd, "\n") < 0)
goto write_err_out;
}
- copy_begin = store.offset[i];
+ copy_begin = replace_end;
}
/* write the pair (value == NULL means unset) */
if (value != NULL) {
- if (store.state == START) {
- if (write_section(fd, key) < 0)
+ if (!store.section_seen) {
+ if (write_section(fd, key, &store) < 0)
goto write_err_out;
}
- if (write_pair(fd, key, value) < 0)
+ if (write_pair(fd, key, value, &store) < 0)
goto write_err_out;
}
/* if new_name == NULL, the section is removed instead */
static int git_config_copy_or_rename_section_in_file(const char *config_filename,
- const char *old_name, const char *new_name, int copy)
+ const char *old_name,
+ const char *new_name, int copy)
{
int ret = 0, remove = 0;
char *filename_buf = NULL;
FILE *config_file = NULL;
struct stat st;
struct strbuf copystr = STRBUF_INIT;
+ struct config_store_data store;
+
+ memset(&store, 0, sizeof(store));
if (new_name && !section_name_is_ok(new_name)) {
ret = error("invalid section name: %s", new_name);
}
store.baselen = strlen(new_name);
if (!copy) {
- if (write_section(out_fd, new_name) < 0) {
+ if (write_section(out_fd, new_name, &store) < 0) {
ret = write_error(get_lock_file_path(&lock));
goto out;
}
output[0] = '\t';
}
} else {
- copystr = store_create_section(new_name);
+ copystr = store_create_section(new_name, &store);
}
}
remove = 0;