summaryrefslogtreecommitdiff
path: root/src/libgit2/config_file.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/libgit2/config_file.c')
-rw-r--r--src/libgit2/config_file.c1194
1 files changed, 1194 insertions, 0 deletions
diff --git a/src/libgit2/config_file.c b/src/libgit2/config_file.c
new file mode 100644
index 000000000..66fcb8ae2
--- /dev/null
+++ b/src/libgit2/config_file.c
@@ -0,0 +1,1194 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include "config.h"
+
+#include "git2/config.h"
+#include "git2/sys/config.h"
+
+#include "array.h"
+#include "str.h"
+#include "config_backend.h"
+#include "config_entries.h"
+#include "config_parse.h"
+#include "filebuf.h"
+#include "regexp.h"
+#include "sysdir.h"
+#include "wildmatch.h"
+#include "hash.h"
+
+/* Max depth for [include] directives */
+#define MAX_INCLUDE_DEPTH 10
+
+typedef struct config_file {
+ git_futils_filestamp stamp;
+ unsigned char checksum[GIT_HASH_SHA1_SIZE];
+ char *path;
+ git_array_t(struct config_file) includes;
+} config_file;
+
+typedef struct {
+ git_config_backend parent;
+ git_mutex values_mutex;
+ git_config_entries *entries;
+ const git_repository *repo;
+ git_config_level_t level;
+
+ git_array_t(git_config_parser) readers;
+
+ bool locked;
+ git_filebuf locked_buf;
+ git_str locked_content;
+
+ config_file file;
+} config_file_backend;
+
+typedef struct {
+ const git_repository *repo;
+ config_file *file;
+ git_config_entries *entries;
+ git_config_level_t level;
+ unsigned int depth;
+} config_file_parse_data;
+
+static int config_file_read(git_config_entries *entries, const git_repository *repo, config_file *file, git_config_level_t level, int depth);
+static int config_file_read_buffer(git_config_entries *entries, const git_repository *repo, config_file *file, git_config_level_t level, int depth, const char *buf, size_t buflen);
+static int config_file_write(config_file_backend *cfg, const char *orig_key, const char *key, const git_regexp *preg, const char *value);
+static char *escape_value(const char *ptr);
+
+/**
+ * Take the current values map from the backend and increase its
+ * refcount. This is its own function to make sure we use the mutex to
+ * avoid the map pointer from changing under us.
+ */
+static int config_file_entries_take(git_config_entries **out, config_file_backend *b)
+{
+ int error;
+
+ if ((error = git_mutex_lock(&b->values_mutex)) < 0) {
+ git_error_set(GIT_ERROR_OS, "failed to lock config backend");
+ return error;
+ }
+
+ git_config_entries_incref(b->entries);
+ *out = b->entries;
+
+ git_mutex_unlock(&b->values_mutex);
+
+ return 0;
+}
+
+static void config_file_clear(config_file *file)
+{
+ config_file *include;
+ uint32_t i;
+
+ if (file == NULL)
+ return;
+
+ git_array_foreach(file->includes, i, include) {
+ config_file_clear(include);
+ }
+ git_array_clear(file->includes);
+
+ git__free(file->path);
+}
+
+static int config_file_open(git_config_backend *cfg, git_config_level_t level, const git_repository *repo)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ int res;
+
+ b->level = level;
+ b->repo = repo;
+
+ if ((res = git_config_entries_new(&b->entries)) < 0)
+ return res;
+
+ if (!git_fs_path_exists(b->file.path))
+ return 0;
+
+ /*
+ * git silently ignores configuration files that are not
+ * readable. We emulate that behavior. This is particularly
+ * important for sandboxed applications on macOS where the
+ * git configuration files may not be readable.
+ */
+ if (p_access(b->file.path, R_OK) < 0)
+ return GIT_ENOTFOUND;
+
+ if (res < 0 || (res = config_file_read(b->entries, repo, &b->file, level, 0)) < 0) {
+ git_config_entries_free(b->entries);
+ b->entries = NULL;
+ }
+
+ return res;
+}
+
+static int config_file_is_modified(int *modified, config_file *file)
+{
+ config_file *include;
+ git_str buf = GIT_STR_INIT;
+ unsigned char checksum[GIT_HASH_SHA1_SIZE];
+ uint32_t i;
+ int error = 0;
+
+ *modified = 0;
+
+ if (!git_futils_filestamp_check(&file->stamp, file->path))
+ goto check_includes;
+
+ if ((error = git_futils_readbuffer(&buf, file->path)) < 0)
+ goto out;
+
+ if ((error = git_hash_buf(checksum, buf.ptr, buf.size, GIT_HASH_ALGORITHM_SHA1)) < 0)
+ goto out;
+
+ if (memcmp(checksum, file->checksum, GIT_HASH_SHA1_SIZE) != 0) {
+ *modified = 1;
+ goto out;
+ }
+
+check_includes:
+ git_array_foreach(file->includes, i, include) {
+ if ((error = config_file_is_modified(modified, include)) < 0 || *modified)
+ goto out;
+ }
+
+out:
+ git_str_dispose(&buf);
+
+ return error;
+}
+
+static void config_file_clear_includes(config_file_backend *cfg)
+{
+ config_file *include;
+ uint32_t i;
+
+ git_array_foreach(cfg->file.includes, i, include)
+ config_file_clear(include);
+ git_array_clear(cfg->file.includes);
+}
+
+static int config_file_set_entries(git_config_backend *cfg, git_config_entries *entries)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ git_config_entries *old = NULL;
+ int error;
+
+ if (b->parent.readonly) {
+ git_error_set(GIT_ERROR_CONFIG, "this backend is read-only");
+ return -1;
+ }
+
+ if ((error = git_mutex_lock(&b->values_mutex)) < 0) {
+ git_error_set(GIT_ERROR_OS, "failed to lock config backend");
+ goto out;
+ }
+
+ old = b->entries;
+ b->entries = entries;
+
+ git_mutex_unlock(&b->values_mutex);
+
+out:
+ git_config_entries_free(old);
+ return error;
+}
+
+static int config_file_refresh_from_buffer(git_config_backend *cfg, const char *buf, size_t buflen)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ git_config_entries *entries = NULL;
+ int error;
+
+ config_file_clear_includes(b);
+
+ if ((error = git_config_entries_new(&entries)) < 0 ||
+ (error = config_file_read_buffer(entries, b->repo, &b->file,
+ b->level, 0, buf, buflen)) < 0 ||
+ (error = config_file_set_entries(cfg, entries)) < 0)
+ goto out;
+
+ entries = NULL;
+out:
+ git_config_entries_free(entries);
+ return error;
+}
+
+static int config_file_refresh(git_config_backend *cfg)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ git_config_entries *entries = NULL;
+ int error, modified;
+
+ if (cfg->readonly)
+ return 0;
+
+ if ((error = config_file_is_modified(&modified, &b->file)) < 0 && error != GIT_ENOTFOUND)
+ goto out;
+
+ if (!modified)
+ return 0;
+
+ config_file_clear_includes(b);
+
+ if ((error = git_config_entries_new(&entries)) < 0 ||
+ (error = config_file_read(entries, b->repo, &b->file, b->level, 0)) < 0 ||
+ (error = config_file_set_entries(cfg, entries)) < 0)
+ goto out;
+
+ entries = NULL;
+out:
+ git_config_entries_free(entries);
+
+ return (error == GIT_ENOTFOUND) ? 0 : error;
+}
+
+static void config_file_free(git_config_backend *_backend)
+{
+ config_file_backend *backend = GIT_CONTAINER_OF(_backend, config_file_backend, parent);
+
+ if (backend == NULL)
+ return;
+
+ config_file_clear(&backend->file);
+ git_config_entries_free(backend->entries);
+ git_mutex_free(&backend->values_mutex);
+ git__free(backend);
+}
+
+static int config_file_iterator(
+ git_config_iterator **iter,
+ struct git_config_backend *backend)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(backend, config_file_backend, parent);
+ git_config_entries *dupped = NULL, *entries = NULL;
+ int error;
+
+ if ((error = config_file_refresh(backend)) < 0 ||
+ (error = config_file_entries_take(&entries, b)) < 0 ||
+ (error = git_config_entries_dup(&dupped, entries)) < 0 ||
+ (error = git_config_entries_iterator_new(iter, dupped)) < 0)
+ goto out;
+
+out:
+ /* Let iterator delete duplicated entries when it's done */
+ git_config_entries_free(entries);
+ git_config_entries_free(dupped);
+ return error;
+}
+
+static int config_file_snapshot(git_config_backend **out, git_config_backend *backend)
+{
+ return git_config_backend_snapshot(out, backend);
+}
+
+static int config_file_set(git_config_backend *cfg, const char *name, const char *value)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ git_config_entries *entries;
+ git_config_entry *existing;
+ char *key, *esc_value = NULL;
+ int error;
+
+ if ((error = git_config__normalize_name(name, &key)) < 0)
+ return error;
+
+ if ((error = config_file_entries_take(&entries, b)) < 0)
+ return error;
+
+ /* Check whether we'd be modifying an included or multivar key */
+ if ((error = git_config_entries_get_unique(&existing, entries, key)) < 0) {
+ if (error != GIT_ENOTFOUND)
+ goto out;
+ error = 0;
+ } else if ((!existing->value && !value) ||
+ (existing->value && value && !strcmp(existing->value, value))) {
+ /* don't update if old and new values already match */
+ error = 0;
+ goto out;
+ }
+
+ /* No early returns due to sanity checks, let's write it out and refresh */
+ if (value) {
+ esc_value = escape_value(value);
+ GIT_ERROR_CHECK_ALLOC(esc_value);
+ }
+
+ if ((error = config_file_write(b, name, key, NULL, esc_value)) < 0)
+ goto out;
+
+out:
+ git_config_entries_free(entries);
+ git__free(esc_value);
+ git__free(key);
+ return error;
+}
+
+/* release the map containing the entry as an equivalent to freeing it */
+static void config_file_entry_free(git_config_entry *entry)
+{
+ git_config_entries *entries = (git_config_entries *) entry->payload;
+ git_config_entries_free(entries);
+}
+
+/*
+ * Internal function that actually gets the value in string form
+ */
+static int config_file_get(git_config_backend *cfg, const char *key, git_config_entry **out)
+{
+ config_file_backend *h = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ git_config_entries *entries = NULL;
+ git_config_entry *entry;
+ int error = 0;
+
+ if (!h->parent.readonly && ((error = config_file_refresh(cfg)) < 0))
+ return error;
+
+ if ((error = config_file_entries_take(&entries, h)) < 0)
+ return error;
+
+ if ((error = (git_config_entries_get(&entry, entries, key))) < 0) {
+ git_config_entries_free(entries);
+ return error;
+ }
+
+ entry->free = config_file_entry_free;
+ entry->payload = entries;
+ *out = entry;
+
+ return 0;
+}
+
+static int config_file_set_multivar(
+ git_config_backend *cfg, const char *name, const char *regexp, const char *value)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ git_regexp preg;
+ int result;
+ char *key;
+
+ GIT_ASSERT_ARG(regexp);
+
+ if ((result = git_config__normalize_name(name, &key)) < 0)
+ return result;
+
+ if ((result = git_regexp_compile(&preg, regexp, 0)) < 0)
+ goto out;
+
+ /* If we do have it, set call config_file_write() and reload */
+ if ((result = config_file_write(b, name, key, &preg, value)) < 0)
+ goto out;
+
+out:
+ git__free(key);
+ git_regexp_dispose(&preg);
+
+ return result;
+}
+
+static int config_file_delete(git_config_backend *cfg, const char *name)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ git_config_entries *entries = NULL;
+ git_config_entry *entry;
+ char *key = NULL;
+ int error;
+
+ if ((error = git_config__normalize_name(name, &key)) < 0)
+ goto out;
+
+ if ((error = config_file_entries_take(&entries, b)) < 0)
+ goto out;
+
+ /* Check whether we'd be modifying an included or multivar key */
+ if ((error = git_config_entries_get_unique(&entry, entries, key)) < 0) {
+ if (error == GIT_ENOTFOUND)
+ git_error_set(GIT_ERROR_CONFIG, "could not find key '%s' to delete", name);
+ goto out;
+ }
+
+ if ((error = config_file_write(b, name, entry->name, NULL, NULL)) < 0)
+ goto out;
+
+out:
+ git_config_entries_free(entries);
+ git__free(key);
+ return error;
+}
+
+static int config_file_delete_multivar(git_config_backend *cfg, const char *name, const char *regexp)
+{
+ config_file_backend *b = GIT_CONTAINER_OF(cfg, config_file_backend, parent);
+ git_config_entries *entries = NULL;
+ git_config_entry *entry = NULL;
+ git_regexp preg = GIT_REGEX_INIT;
+ char *key = NULL;
+ int result;
+
+ if ((result = git_config__normalize_name(name, &key)) < 0)
+ goto out;
+
+ if ((result = config_file_entries_take(&entries, b)) < 0)
+ goto out;
+
+ if ((result = git_config_entries_get(&entry, entries, key)) < 0) {
+ if (result == GIT_ENOTFOUND)
+ git_error_set(GIT_ERROR_CONFIG, "could not find key '%s' to delete", name);
+ goto out;
+ }
+
+ if ((result = git_regexp_compile(&preg, regexp, 0)) < 0)
+ goto out;
+
+ if ((result = config_file_write(b, name, key, &preg, NULL)) < 0)
+ goto out;
+
+out:
+ git_config_entries_free(entries);
+ git__free(key);
+ git_regexp_dispose(&preg);
+ return result;
+}
+
+static int config_file_lock(git_config_backend *_cfg)
+{
+ config_file_backend *cfg = GIT_CONTAINER_OF(_cfg, config_file_backend, parent);
+ int error;
+
+ if ((error = git_filebuf_open(&cfg->locked_buf, cfg->file.path, 0, GIT_CONFIG_FILE_MODE)) < 0)
+ return error;
+
+ error = git_futils_readbuffer(&cfg->locked_content, cfg->file.path);
+ if (error < 0 && error != GIT_ENOTFOUND) {
+ git_filebuf_cleanup(&cfg->locked_buf);
+ return error;
+ }
+
+ cfg->locked = true;
+ return 0;
+
+}
+
+static int config_file_unlock(git_config_backend *_cfg, int success)
+{
+ config_file_backend *cfg = GIT_CONTAINER_OF(_cfg, config_file_backend, parent);
+ int error = 0;
+
+ if (success) {
+ git_filebuf_write(&cfg->locked_buf, cfg->locked_content.ptr, cfg->locked_content.size);
+ error = git_filebuf_commit(&cfg->locked_buf);
+ }
+
+ git_filebuf_cleanup(&cfg->locked_buf);
+ git_str_dispose(&cfg->locked_content);
+ cfg->locked = false;
+
+ return error;
+}
+
+int git_config_backend_from_file(git_config_backend **out, const char *path)
+{
+ config_file_backend *backend;
+
+ backend = git__calloc(1, sizeof(config_file_backend));
+ GIT_ERROR_CHECK_ALLOC(backend);
+
+ backend->parent.version = GIT_CONFIG_BACKEND_VERSION;
+ git_mutex_init(&backend->values_mutex);
+
+ backend->file.path = git__strdup(path);
+ GIT_ERROR_CHECK_ALLOC(backend->file.path);
+ git_array_init(backend->file.includes);
+
+ backend->parent.open = config_file_open;
+ backend->parent.get = config_file_get;
+ backend->parent.set = config_file_set;
+ backend->parent.set_multivar = config_file_set_multivar;
+ backend->parent.del = config_file_delete;
+ backend->parent.del_multivar = config_file_delete_multivar;
+ backend->parent.iterator = config_file_iterator;
+ backend->parent.snapshot = config_file_snapshot;
+ backend->parent.lock = config_file_lock;
+ backend->parent.unlock = config_file_unlock;
+ backend->parent.free = config_file_free;
+
+ *out = (git_config_backend *)backend;
+
+ return 0;
+}
+
+static int included_path(git_str *out, const char *dir, const char *path)
+{
+ /* From the user's home */
+ if (path[0] == '~' && path[1] == '/')
+ return git_sysdir_expand_global_file(out, &path[1]);
+
+ return git_fs_path_join_unrooted(out, path, dir, NULL);
+}
+
+/* Escape the values to write them to the file */
+static char *escape_value(const char *ptr)
+{
+ git_str buf;
+ size_t len;
+ const char *esc;
+
+ GIT_ASSERT_ARG_WITH_RETVAL(ptr, NULL);
+
+ len = strlen(ptr);
+ if (!len)
+ return git__calloc(1, sizeof(char));
+
+ if (git_str_init(&buf, len) < 0)
+ return NULL;
+
+ while (*ptr != '\0') {
+ if ((esc = strchr(git_config_escaped, *ptr)) != NULL) {
+ git_str_putc(&buf, '\\');
+ git_str_putc(&buf, git_config_escapes[esc - git_config_escaped]);
+ } else {
+ git_str_putc(&buf, *ptr);
+ }
+ ptr++;
+ }
+
+ if (git_str_oom(&buf))
+ return NULL;
+
+ return git_str_detach(&buf);
+}
+
+static int parse_include(config_file_parse_data *parse_data, const char *file)
+{
+ config_file *include;
+ git_str path = GIT_STR_INIT;
+ char *dir;
+ int result;
+
+ if (!file)
+ return 0;
+
+ if ((result = git_fs_path_dirname_r(&path, parse_data->file->path)) < 0)
+ return result;
+
+ dir = git_str_detach(&path);
+ result = included_path(&path, dir, file);
+ git__free(dir);
+
+ if (result < 0)
+ return result;
+
+ include = git_array_alloc(parse_data->file->includes);
+ GIT_ERROR_CHECK_ALLOC(include);
+ memset(include, 0, sizeof(*include));
+ git_array_init(include->includes);
+ include->path = git_str_detach(&path);
+
+ result = config_file_read(parse_data->entries, parse_data->repo, include,
+ parse_data->level, parse_data->depth+1);
+
+ if (result == GIT_ENOTFOUND) {
+ git_error_clear();
+ result = 0;
+ }
+
+ return result;
+}
+
+static int do_match_gitdir(
+ int *matches,
+ const git_repository *repo,
+ const char *cfg_file,
+ const char *condition,
+ bool case_insensitive)
+{
+ git_str pattern = GIT_STR_INIT, gitdir = GIT_STR_INIT;
+ int error;
+
+ if (condition[0] == '.' && git_fs_path_is_dirsep(condition[1])) {
+ git_fs_path_dirname_r(&pattern, cfg_file);
+ git_str_joinpath(&pattern, pattern.ptr, condition + 2);
+ } else if (condition[0] == '~' && git_fs_path_is_dirsep(condition[1]))
+ git_sysdir_expand_global_file(&pattern, condition + 1);
+ else if (!git_fs_path_is_absolute(condition))
+ git_str_joinpath(&pattern, "**", condition);
+ else
+ git_str_sets(&pattern, condition);
+
+ if (git_fs_path_is_dirsep(condition[strlen(condition) - 1]))
+ git_str_puts(&pattern, "**");
+
+ if (git_str_oom(&pattern)) {
+ error = -1;
+ goto out;
+ }
+
+ if ((error = git_repository__item_path(&gitdir, repo, GIT_REPOSITORY_ITEM_GITDIR)) < 0)
+ goto out;
+
+ if (git_fs_path_is_dirsep(gitdir.ptr[gitdir.size - 1]))
+ git_str_truncate(&gitdir, gitdir.size - 1);
+
+ *matches = wildmatch(pattern.ptr, gitdir.ptr,
+ WM_PATHNAME | (case_insensitive ? WM_CASEFOLD : 0)) == WM_MATCH;
+out:
+ git_str_dispose(&pattern);
+ git_str_dispose(&gitdir);
+ return error;
+}
+
+static int conditional_match_gitdir(
+ int *matches,
+ const git_repository *repo,
+ const char *cfg_file,
+ const char *value)
+{
+ return do_match_gitdir(matches, repo, cfg_file, value, false);
+}
+
+static int conditional_match_gitdir_i(
+ int *matches,
+ const git_repository *repo,
+ const char *cfg_file,
+ const char *value)
+{
+ return do_match_gitdir(matches, repo, cfg_file, value, true);
+}
+
+static int conditional_match_onbranch(
+ int *matches,
+ const git_repository *repo,
+ const char *cfg_file,
+ const char *condition)
+{
+ git_str reference = GIT_STR_INIT, buf = GIT_STR_INIT;
+ int error;
+
+ GIT_UNUSED(cfg_file);
+
+ /*
+ * NOTE: you cannot use `git_repository_head` here. Looking up the
+ * HEAD reference will create the ODB, which causes us to read the
+ * repo's config for keys like core.precomposeUnicode. As we're
+ * just parsing the config right now, though, this would result in
+ * an endless recursion.
+ */
+
+ if ((error = git_str_joinpath(&buf, git_repository_path(repo), GIT_HEAD_FILE)) < 0 ||
+ (error = git_futils_readbuffer(&reference, buf.ptr)) < 0)
+ goto out;
+ git_str_rtrim(&reference);
+
+ if (git__strncmp(reference.ptr, GIT_SYMREF, strlen(GIT_SYMREF)))
+ goto out;
+ git_str_consume(&reference, reference.ptr + strlen(GIT_SYMREF));
+
+ if (git__strncmp(reference.ptr, GIT_REFS_HEADS_DIR, strlen(GIT_REFS_HEADS_DIR)))
+ goto out;
+ git_str_consume(&reference, reference.ptr + strlen(GIT_REFS_HEADS_DIR));
+
+ /*
+ * If the condition ends with a '/', then we should treat it as if
+ * it had '**' appended.
+ */
+ if ((error = git_str_sets(&buf, condition)) < 0)
+ goto out;
+ if (git_fs_path_is_dirsep(condition[strlen(condition) - 1]) &&
+ (error = git_str_puts(&buf, "**")) < 0)
+ goto out;
+
+ *matches = wildmatch(buf.ptr, reference.ptr, WM_PATHNAME) == WM_MATCH;
+out:
+ git_str_dispose(&reference);
+ git_str_dispose(&buf);
+
+ return error;
+
+}
+
+static const struct {
+ const char *prefix;
+ int (*matches)(int *matches, const git_repository *repo, const char *cfg, const char *value);
+} conditions[] = {
+ { "gitdir:", conditional_match_gitdir },
+ { "gitdir/i:", conditional_match_gitdir_i },
+ { "onbranch:", conditional_match_onbranch }
+};
+
+static int parse_conditional_include(config_file_parse_data *parse_data, const char *section, const char *file)
+{
+ char *condition;
+ size_t section_len, i;
+ int error = 0, matches;
+
+ if (!parse_data->repo || !file)
+ return 0;
+
+ section_len = strlen(section);
+
+ /*
+ * We checked that the string starts with `includeIf.` and ends
+ * in `.path` to get here. Make sure it consists of more.
+ */
+ if (section_len < CONST_STRLEN("includeIf.") + CONST_STRLEN(".path"))
+ return 0;
+
+ condition = git__substrdup(section + CONST_STRLEN("includeIf."),
+ section_len - CONST_STRLEN("includeIf.") - CONST_STRLEN(".path"));
+
+ GIT_ERROR_CHECK_ALLOC(condition);
+
+ for (i = 0; i < ARRAY_SIZE(conditions); i++) {
+ if (git__prefixcmp(condition, conditions[i].prefix))
+ continue;
+
+ if ((error = conditions[i].matches(&matches,
+ parse_data->repo,
+ parse_data->file->path,
+ condition + strlen(conditions[i].prefix))) < 0)
+ break;
+
+ if (matches)
+ error = parse_include(parse_data, file);
+
+ break;
+ }
+
+ git__free(condition);
+ return error;
+}
+
+static int read_on_variable(
+ git_config_parser *reader,
+ const char *current_section,
+ const char *var_name,
+ const char *var_value,
+ const char *line,
+ size_t line_len,
+ void *data)
+{
+ config_file_parse_data *parse_data = (config_file_parse_data *)data;
+ git_str buf = GIT_STR_INIT;
+ git_config_entry *entry;
+ const char *c;
+ int result = 0;
+
+ GIT_UNUSED(reader);
+ GIT_UNUSED(line);
+ GIT_UNUSED(line_len);
+
+ if (current_section) {
+ /* TODO: Once warnings lang, we should likely warn
+ * here. Git appears to warn in most cases if it sees
+ * un-namespaced config options.
+ */
+ git_str_puts(&buf, current_section);
+ git_str_putc(&buf, '.');
+ }
+
+ for (c = var_name; *c; c++)
+ git_str_putc(&buf, git__tolower(*c));
+
+ if (git_str_oom(&buf))
+ return -1;
+
+ entry = git__calloc(1, sizeof(git_config_entry));
+ GIT_ERROR_CHECK_ALLOC(entry);
+ entry->name = git_str_detach(&buf);
+ entry->value = var_value ? git__strdup(var_value) : NULL;
+ entry->level = parse_data->level;
+ entry->include_depth = parse_data->depth;
+
+ if ((result = git_config_entries_append(parse_data->entries, entry)) < 0)
+ return result;
+
+ result = 0;
+
+ /* Add or append the new config option */
+ if (!git__strcmp(entry->name, "include.path"))
+ result = parse_include(parse_data, entry->value);
+ else if (!git__prefixcmp(entry->name, "includeif.") &&
+ !git__suffixcmp(entry->name, ".path"))
+ result = parse_conditional_include(parse_data, entry->name, entry->value);
+
+ return result;
+}
+
+static int config_file_read_buffer(
+ git_config_entries *entries,
+ const git_repository *repo,
+ config_file *file,
+ git_config_level_t level,
+ int depth,
+ const char *buf,
+ size_t buflen)
+{
+ config_file_parse_data parse_data;
+ git_config_parser reader;
+ int error;
+
+ if (depth >= MAX_INCLUDE_DEPTH) {
+ git_error_set(GIT_ERROR_CONFIG, "maximum config include depth reached");
+ return -1;
+ }
+
+ /* Initialize the reading position */
+ reader.path = file->path;
+ git_parse_ctx_init(&reader.ctx, buf, buflen);
+
+ /* If the file is empty, there's nothing for us to do */
+ if (!reader.ctx.content || *reader.ctx.content == '\0') {
+ error = 0;
+ goto out;
+ }
+
+ parse_data.repo = repo;
+ parse_data.file = file;
+ parse_data.entries = entries;
+ parse_data.level = level;
+ parse_data.depth = depth;
+
+ error = git_config_parse(&reader, NULL, read_on_variable, NULL, NULL, &parse_data);
+
+out:
+ return error;
+}
+
+static int config_file_read(
+ git_config_entries *entries,
+ const git_repository *repo,
+ config_file *file,
+ git_config_level_t level,
+ int depth)
+{
+ git_str contents = GIT_STR_INIT;
+ struct stat st;
+ int error;
+
+ if (p_stat(file->path, &st) < 0) {
+ error = git_fs_path_set_error(errno, file->path, "stat");
+ goto out;
+ }
+
+ if ((error = git_futils_readbuffer(&contents, file->path)) < 0)
+ goto out;
+
+ git_futils_filestamp_set_from_stat(&file->stamp, &st);
+ if ((error = git_hash_buf(file->checksum, contents.ptr, contents.size, GIT_HASH_ALGORITHM_SHA1)) < 0)
+ goto out;
+
+ if ((error = config_file_read_buffer(entries, repo, file, level, depth,
+ contents.ptr, contents.size)) < 0)
+ goto out;
+
+out:
+ git_str_dispose(&contents);
+ return error;
+}
+
+static int write_section(git_str *fbuf, const char *key)
+{
+ int result;
+ const char *dot;
+ git_str buf = GIT_STR_INIT;
+
+ /* All of this just for [section "subsection"] */
+ dot = strchr(key, '.');
+ git_str_putc(&buf, '[');
+ if (dot == NULL) {
+ git_str_puts(&buf, key);
+ } else {
+ char *escaped;
+ git_str_put(&buf, key, dot - key);
+ escaped = escape_value(dot + 1);
+ GIT_ERROR_CHECK_ALLOC(escaped);
+ git_str_printf(&buf, " \"%s\"", escaped);
+ git__free(escaped);
+ }
+ git_str_puts(&buf, "]\n");
+
+ if (git_str_oom(&buf))
+ return -1;
+
+ result = git_str_put(fbuf, git_str_cstr(&buf), buf.size);
+ git_str_dispose(&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_str *buf;
+ git_str buffered_comment;
+ unsigned int in_section : 1,
+ preg_replaced : 1;
+ const char *orig_section;
+ const char *section;
+ const char *orig_name;
+ const char *name;
+ const git_regexp *preg;
+ const char *value;
+};
+
+static int write_line_to(git_str *buf, const char *line, size_t line_len)
+{
+ int result = git_str_put(buf, line, line_len);
+
+ if (!result && line_len && line[line_len-1] != '\n')
+ result = git_str_printf(buf, "\n");
+
+ return result;
+}
+
+static int write_line(struct write_data *write_data, const char *line, size_t line_len)
+{
+ return write_line_to(write_data->buf, line, line_len);
+}
+
+static int write_value(struct write_data *write_data)
+{
+ const char *q;
+ int result;
+
+ q = quotes_for_value(write_data->value);
+ result = git_str_printf(write_data->buf,
+ "\t%s = %s%s%s\n", write_data->orig_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(
+ git_config_parser *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 there were comments just before this section, dump them as well.
+ */
+ if (!result) {
+ result = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size);
+ git_str_clear(&write_data->buffered_comment);
+ }
+
+ if (!result)
+ result = write_line(write_data, line, line_len);
+
+ return result;
+}
+
+static int write_on_variable(
+ git_config_parser *reader,
+ const char *current_section,
+ const char *var_name,
+ const 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;
+ int error;
+
+ GIT_UNUSED(reader);
+ GIT_UNUSED(current_section);
+
+ /*
+ * If there were comments just before this variable, let's dump them as well.
+ */
+ if ((error = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size)) < 0)
+ return error;
+
+ git_str_clear(&write_data->buffered_comment);
+
+ /* 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 = (git_regexp_match(write_data->preg, var_value) == 0);
+
+ /* 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(git_config_parser *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_to(&write_data->buffered_comment, line, line_len);
+}
+
+static int write_on_eof(
+ git_config_parser *reader, const char *current_section, void *data)
+{
+ struct write_data *write_data = (struct write_data *)data;
+ int result = 0;
+
+ GIT_UNUSED(reader);
+
+ /*
+ * If we've buffered comments when reaching EOF, make sure to dump them.
+ */
+ if ((result = git_str_put(write_data->buf, write_data->buffered_comment.ptr, write_data->buffered_comment.size)) < 0)
+ return result;
+
+ /* 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) {
+ /* write the section header unless we're already in it */
+ if (!current_section || strcmp(current_section, write_data->section))
+ result = write_section(write_data->buf, write_data->orig_section);
+
+ if (!result)
+ 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_file_write(config_file_backend *cfg, const char *orig_key, const char *key, const git_regexp *preg, const char *value)
+
+{
+ char *orig_section = NULL, *section = NULL, *orig_name, *name, *ldot;
+ git_str buf = GIT_STR_INIT, contents = GIT_STR_INIT;
+ git_config_parser parser = GIT_CONFIG_PARSER_INIT;
+ git_filebuf file = GIT_FILEBUF_INIT;
+ struct write_data write_data;
+ int error;
+
+ memset(&write_data, 0, sizeof(write_data));
+
+ if (cfg->locked) {
+ error = git_str_puts(&contents, git_str_cstr(&cfg->locked_content) == NULL ? "" : git_str_cstr(&cfg->locked_content));
+ } else {
+ if ((error = git_filebuf_open(&file, cfg->file.path, GIT_FILEBUF_HASH_CONTENTS,
+ GIT_CONFIG_FILE_MODE)) < 0)
+ goto done;
+
+ /* We need to read in our own config file */
+ error = git_futils_readbuffer(&contents, cfg->file.path);
+ }
+ if (error < 0 && error != GIT_ENOTFOUND)
+ goto done;
+
+ if ((git_config_parser_init(&parser, cfg->file.path, contents.ptr, contents.size)) < 0)
+ goto done;
+
+ ldot = strrchr(key, '.');
+ name = ldot + 1;
+ section = git__strndup(key, ldot - key);
+ GIT_ERROR_CHECK_ALLOC(section);
+
+ ldot = strrchr(orig_key, '.');
+ orig_name = ldot + 1;
+ orig_section = git__strndup(orig_key, ldot - orig_key);
+ GIT_ERROR_CHECK_ALLOC(orig_section);
+
+ write_data.buf = &buf;
+ write_data.orig_section = orig_section;
+ write_data.section = section;
+ write_data.orig_name = orig_name;
+ write_data.name = name;
+ write_data.preg = preg;
+ write_data.value = value;
+
+ if ((error = git_config_parse(&parser, write_on_section, write_on_variable,
+ write_on_comment, write_on_eof, &write_data)) < 0)
+ goto done;
+
+ if (cfg->locked) {
+ size_t len = buf.asize;
+ /* Update our copy with the modified contents */
+ git_str_dispose(&cfg->locked_content);
+ git_str_attach(&cfg->locked_content, git_str_detach(&buf), len);
+ } else {
+ git_filebuf_write(&file, git_str_cstr(&buf), git_str_len(&buf));
+
+ if ((error = git_filebuf_commit(&file)) < 0)
+ goto done;
+
+ if ((error = config_file_refresh_from_buffer(&cfg->parent, buf.ptr, buf.size)) < 0)
+ goto done;
+ }
+
+done:
+ git__free(section);
+ git__free(orig_section);
+ git_str_dispose(&write_data.buffered_comment);
+ git_str_dispose(&buf);
+ git_str_dispose(&contents);
+ git_filebuf_cleanup(&file);
+ git_config_parser_dispose(&parser);
+
+ return error;
+}