diff options
author | Junio C Hamano <gitster@pobox.com> | 2012-02-23 13:30:14 -0800 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2012-02-23 13:30:14 -0800 |
commit | fd1727f5fa1ca1817eee36f99c9245dc2217ecac (patch) | |
tree | 20f7bbc16c723bf42a6736f6719b22d5db06b30f | |
parent | Merge branch 'jc/add-refresh-unmerged' (diff) | |
parent | config: add include directive (diff) | |
download | tgif-fd1727f5fa1ca1817eee36f99c9245dc2217ecac.tar.xz |
Merge branch 'jk/config-include'
* jk/config-include:
: An assignment to the include.path pseudo-variable causes the named file
: to be included in-place when Git looks up configuration variables.
config: add include directive
config: eliminate config_exclusive_filename
config: stop using config_exclusive_filename
config: provide a version of git_config with more options
config: teach git_config_rename_section a file argument
config: teach git_config_set_multivar_in_file a default path
config: copy the return value of prefix_filename
t1300: add missing &&-chaining
docs/api-config: minor clarifications
docs: add a basic description of the config API
-rw-r--r-- | Documentation/config.txt | 15 | ||||
-rw-r--r-- | Documentation/git-config.txt | 5 | ||||
-rw-r--r-- | Documentation/technical/api-config.txt | 140 | ||||
-rw-r--r-- | builtin/config.c | 96 | ||||
-rw-r--r-- | cache.h | 11 | ||||
-rw-r--r-- | config.c | 127 | ||||
-rwxr-xr-x | t/t1300-repo-config.sh | 27 | ||||
-rwxr-xr-x | t/t1305-config-include.sh | 134 |
8 files changed, 495 insertions, 60 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt index abeb82b2c6..e55dae1806 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -84,6 +84,17 @@ customary UNIX fashion. Some variables may require a special value format. +Includes +~~~~~~~~ + +You can include one config file from another by setting the special +`include.path` variable to the name of the file to be included. The +included file is expanded immediately, as if its contents had been +found at the location of the include directive. If the value of the +`include.path` variable is a relative path, the path is considered to be +relative to the configuration file in which the include directive was +found. See below for examples. + Example ~~~~~~~ @@ -106,6 +117,10 @@ Example gitProxy="ssh" for "kernel.org" gitProxy=default-proxy ; for the rest + [include] + path = /path/to/foo.inc ; include by absolute path + path = foo ; expand "foo" relative to the current file + Variables ~~~~~~~~~ diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt index e7ecf5d803..aa8303b1ad 100644 --- a/Documentation/git-config.txt +++ b/Documentation/git-config.txt @@ -178,6 +178,11 @@ See also <<FILES>>. Opens an editor to modify the specified config file; either '--system', '--global', or repository (default). +--includes:: +--no-includes:: + Respect `include.*` directives in config files when looking up + values. Defaults to on. + [[FILES]] FILES ----- diff --git a/Documentation/technical/api-config.txt b/Documentation/technical/api-config.txt new file mode 100644 index 0000000000..edf8dfb99b --- /dev/null +++ b/Documentation/technical/api-config.txt @@ -0,0 +1,140 @@ +config API +========== + +The config API gives callers a way to access git configuration files +(and files which have the same syntax). See linkgit:git-config[1] for a +discussion of the config file syntax. + +General Usage +------------- + +Config files are parsed linearly, and each variable found is passed to a +caller-provided callback function. The callback function is responsible +for any actions to be taken on the config option, and is free to ignore +some options. It is not uncommon for the configuration to be parsed +several times during the run of a git program, with different callbacks +picking out different variables useful to themselves. + +A config callback function takes three parameters: + +- the name of the parsed variable. This is in canonical "flat" form: the + section, subsection, and variable segments will be separated by dots, + and the section and variable segments will be all lowercase. E.g., + `core.ignorecase`, `diff.SomeType.textconv`. + +- the value of the found variable, as a string. If the variable had no + value specified, the value will be NULL (typically this means it + should be interpreted as boolean true). + +- a void pointer passed in by the caller of the config API; this can + contain callback-specific data + +A config callback should return 0 for success, or -1 if the variable +could not be parsed properly. + +Basic Config Querying +--------------------- + +Most programs will simply want to look up variables in all config files +that git knows about, using the normal precedence rules. To do this, +call `git_config` with a callback function and void data pointer. + +`git_config` will read all config sources in order of increasing +priority. Thus a callback should typically overwrite previously-seen +entries with new ones (e.g., if both the user-wide `~/.gitconfig` and +repo-specific `.git/config` contain `color.ui`, the config machinery +will first feed the user-wide one to the callback, and then the +repo-specific one; by overwriting, the higher-priority repo-specific +value is left at the end). + +The `git_config_with_options` function lets the caller examine config +while adjusting some of the default behavior of `git_config`. It should +almost never be used by "regular" git code that is looking up +configuration variables. It is intended for advanced callers like +`git-config`, which are intentionally tweaking the normal config-lookup +process. It takes two extra parameters: + +`filename`:: +If this parameter is non-NULL, it specifies the name of a file to +parse for configuration, rather than looking in the usual files. Regular +`git_config` defaults to `NULL`. + +`respect_includes`:: +Specify whether include directives should be followed in parsed files. +Regular `git_config` defaults to `1`. + +There is a special version of `git_config` called `git_config_early`. +This version takes an additional parameter to specify the repository +config, instead of having it looked up via `git_path`. This is useful +early in a git program before the repository has been found. Unless +you're working with early setup code, you probably don't want to use +this. + +Reading Specific Files +---------------------- + +To read a specific file in git-config format, use +`git_config_from_file`. This takes the same callback and data parameters +as `git_config`. + +Value Parsing Helpers +--------------------- + +To aid in parsing string values, the config API provides callbacks with +a number of helper functions, including: + +`git_config_int`:: +Parse the string to an integer, including unit factors. Dies on error; +otherwise, returns the parsed result. + +`git_config_ulong`:: +Identical to `git_config_int`, but for unsigned longs. + +`git_config_bool`:: +Parse a string into a boolean value, respecting keywords like "true" and +"false". Integer values are converted into true/false values (when they +are non-zero or zero, respectively). Other values cause a die(). If +parsing is successful, the return value is the result. + +`git_config_bool_or_int`:: +Same as `git_config_bool`, except that integers are returned as-is, and +an `is_bool` flag is unset. + +`git_config_maybe_bool`:: +Same as `git_config_bool`, except that it returns -1 on error rather +than dying. + +`git_config_string`:: +Allocates and copies the value string into the `dest` parameter; if no +string is given, prints an error message and returns -1. + +`git_config_pathname`:: +Similar to `git_config_string`, but expands `~` or `~user` into the +user's home directory when found at the beginning of the path. + +Include Directives +------------------ + +By default, the config parser does not respect include directives. +However, a caller can use the special `git_config_include` wrapper +callback to support them. To do so, you simply wrap your "real" callback +function and data pointer in a `struct config_include_data`, and pass +the wrapper to the regular config-reading functions. For example: + +------------------------------------------- +int read_file_with_include(const char *file, config_fn_t fn, void *data) +{ + struct config_include_data inc = CONFIG_INCLUDE_INIT; + inc.fn = fn; + inc.data = data; + return git_config_from_file(git_config_include, file, &inc); +} +------------------------------------------- + +`git_config` respects includes automatically. The lower-level +`git_config_from_file` does not. + +Writing Config Files +-------------------- + +TODO diff --git a/builtin/config.c b/builtin/config.c index d35c06ae51..d41a9bfb14 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -25,6 +25,7 @@ static const char *given_config_file; static int actions, types; static const char *get_color_slot, *get_colorbool_slot; static int end_null; +static int respect_includes = -1; #define ACTION_GET (1<<0) #define ACTION_GET_ALL (1<<1) @@ -74,6 +75,7 @@ static struct option builtin_config_options[] = { OPT_BIT(0, "path", &types, "value is a path (file or directory name)", TYPE_PATH), OPT_GROUP("Other"), OPT_BOOLEAN('z', "null", &end_null, "terminate values with NUL byte"), + OPT_BOOL(0, "includes", &respect_includes, "respect include directives on lookup"), OPT_END(), }; @@ -161,8 +163,11 @@ static int get_value(const char *key_, const char *regex_) int ret = -1; char *global = NULL, *repo_config = NULL; const char *system_wide = NULL, *local; + struct config_include_data inc = CONFIG_INCLUDE_INIT; + config_fn_t fn; + void *data; - local = config_exclusive_filename; + local = given_config_file; if (!local) { const char *home = getenv("HOME"); local = repo_config = git_pathdup("config"); @@ -213,19 +218,28 @@ static int get_value(const char *key_, const char *regex_) } } + fn = show_config; + data = NULL; + if (respect_includes) { + inc.fn = fn; + inc.data = data; + fn = git_config_include; + data = &inc; + } + if (do_all && system_wide) - git_config_from_file(show_config, system_wide, NULL); + git_config_from_file(fn, system_wide, data); if (do_all && global) - git_config_from_file(show_config, global, NULL); + git_config_from_file(fn, global, data); if (do_all) - git_config_from_file(show_config, local, NULL); - git_config_from_parameters(show_config, NULL); + git_config_from_file(fn, local, data); + git_config_from_parameters(fn, data); if (!do_all && !seen) - git_config_from_file(show_config, local, NULL); + git_config_from_file(fn, local, data); if (!do_all && !seen && global) - git_config_from_file(show_config, global, NULL); + git_config_from_file(fn, global, data); if (!do_all && !seen && system_wide) - git_config_from_file(show_config, system_wide, NULL); + git_config_from_file(fn, system_wide, data); free(key); if (regexp) { @@ -301,7 +315,8 @@ static void get_color(const char *def_color) { get_color_found = 0; parsed_color[0] = '\0'; - git_config(git_get_color_config, NULL); + git_config_with_options(git_get_color_config, NULL, + given_config_file, respect_includes); if (!get_color_found && def_color) color_parse(def_color, "command line", parsed_color); @@ -328,7 +343,8 @@ static int get_colorbool(int print) { get_colorbool_found = -1; get_diff_color_found = -1; - git_config(git_get_colorbool_config, NULL); + git_config_with_options(git_get_colorbool_config, NULL, + given_config_file, respect_includes); if (get_colorbool_found < 0) { if (!strcmp(get_colorbool_slot, "color.diff")) @@ -351,7 +367,7 @@ int cmd_config(int argc, const char **argv, const char *prefix) int nongit = !startup_info->have_repository; char *value; - config_exclusive_filename = getenv(CONFIG_ENVIRONMENT); + given_config_file = getenv(CONFIG_ENVIRONMENT); argc = parse_options(argc, argv, prefix, builtin_config_options, builtin_config_usage, @@ -366,24 +382,28 @@ int cmd_config(int argc, const char **argv, const char *prefix) char *home = getenv("HOME"); if (home) { char *user_config = xstrdup(mkpath("%s/.gitconfig", home)); - config_exclusive_filename = user_config; + given_config_file = user_config; } else { die("$HOME not set"); } } else if (use_system_config) - config_exclusive_filename = git_etc_gitconfig(); + given_config_file = git_etc_gitconfig(); else if (use_local_config) - config_exclusive_filename = git_pathdup("config"); + given_config_file = git_pathdup("config"); else if (given_config_file) { if (!is_absolute_path(given_config_file) && prefix) - config_exclusive_filename = prefix_filename(prefix, - strlen(prefix), - given_config_file); + given_config_file = + xstrdup(prefix_filename(prefix, + strlen(prefix), + given_config_file)); else - config_exclusive_filename = given_config_file; + given_config_file = given_config_file; } + if (respect_includes == -1) + respect_includes = !given_config_file; + if (end_null) { term = '\0'; delim = '\n'; @@ -420,28 +440,30 @@ int cmd_config(int argc, const char **argv, const char *prefix) if (actions == ACTION_LIST) { check_argc(argc, 0, 0); - if (git_config(show_all_config, NULL) < 0) { - if (config_exclusive_filename) + if (git_config_with_options(show_all_config, NULL, + given_config_file, + respect_includes) < 0) { + if (given_config_file) die_errno("unable to read config file '%s'", - config_exclusive_filename); + given_config_file); else die("error processing config file(s)"); } } else if (actions == ACTION_EDIT) { check_argc(argc, 0, 0); - if (!config_exclusive_filename && nongit) + if (!given_config_file && nongit) die("not in a git directory"); git_config(git_default_config, NULL); - launch_editor(config_exclusive_filename ? - config_exclusive_filename : git_path("config"), + launch_editor(given_config_file ? + given_config_file : git_path("config"), NULL, NULL); } else if (actions == ACTION_SET) { int ret; check_argc(argc, 2, 2); value = normalize_value(argv[0], argv[1]); - ret = git_config_set(argv[0], value); + ret = git_config_set_in_file(given_config_file, argv[0], value); if (ret == CONFIG_NOTHING_SET) error("cannot overwrite multiple values with a single value\n" " Use a regexp, --add or --replace-all to change %s.", argv[0]); @@ -450,17 +472,20 @@ int cmd_config(int argc, const char **argv, const char *prefix) else if (actions == ACTION_SET_ALL) { check_argc(argc, 2, 3); value = normalize_value(argv[0], argv[1]); - return git_config_set_multivar(argv[0], value, argv[2], 0); + return git_config_set_multivar_in_file(given_config_file, + argv[0], value, argv[2], 0); } else if (actions == ACTION_ADD) { check_argc(argc, 2, 2); value = normalize_value(argv[0], argv[1]); - return git_config_set_multivar(argv[0], value, "^$", 0); + return git_config_set_multivar_in_file(given_config_file, + argv[0], value, "^$", 0); } else if (actions == ACTION_REPLACE_ALL) { check_argc(argc, 2, 3); value = normalize_value(argv[0], argv[1]); - return git_config_set_multivar(argv[0], value, argv[2], 1); + return git_config_set_multivar_in_file(given_config_file, + argv[0], value, argv[2], 1); } else if (actions == ACTION_GET) { check_argc(argc, 1, 2); @@ -481,18 +506,22 @@ int cmd_config(int argc, const char **argv, const char *prefix) else if (actions == ACTION_UNSET) { check_argc(argc, 1, 2); if (argc == 2) - return git_config_set_multivar(argv[0], NULL, argv[1], 0); + return git_config_set_multivar_in_file(given_config_file, + argv[0], NULL, argv[1], 0); else - return git_config_set(argv[0], NULL); + return git_config_set_in_file(given_config_file, + argv[0], NULL); } else if (actions == ACTION_UNSET_ALL) { check_argc(argc, 1, 2); - return git_config_set_multivar(argv[0], NULL, argv[1], 1); + return git_config_set_multivar_in_file(given_config_file, + argv[0], NULL, argv[1], 1); } else if (actions == ACTION_RENAME_SECTION) { int ret; check_argc(argc, 2, 2); - ret = git_config_rename_section(argv[0], argv[1]); + ret = git_config_rename_section_in_file(given_config_file, + argv[0], argv[1]); if (ret < 0) return ret; if (ret == 0) @@ -501,7 +530,8 @@ int cmd_config(int argc, const char **argv, const char *prefix) else if (actions == ACTION_REMOVE_SECTION) { int ret; check_argc(argc, 1, 1); - ret = git_config_rename_section(argv[0], NULL); + ret = git_config_rename_section_in_file(given_config_file, + argv[0], NULL); if (ret < 0) return ret; if (ret == 0) @@ -1115,6 +1115,8 @@ extern int git_config_from_file(config_fn_t fn, const char *, void *); extern void git_config_push_parameter(const char *text); extern int git_config_from_parameters(config_fn_t fn, void *data); extern int git_config(config_fn_t fn, void *); +extern int git_config_with_options(config_fn_t fn, void *, + const char *filename, int respect_includes); extern int git_config_early(config_fn_t fn, void *, const char *repo_config); extern int git_parse_ulong(const char *, unsigned long *); extern int git_config_int(const char *, const char *); @@ -1130,6 +1132,7 @@ extern int git_config_parse_key(const char *, char **, int *); extern int git_config_set_multivar(const char *, const char *, const char *, int); extern int git_config_set_multivar_in_file(const char *, const char *, const char *, const char *, int); extern int git_config_rename_section(const char *, const char *); +extern int git_config_rename_section_in_file(const char *, const char *, const char *); extern const char *git_etc_gitconfig(void); extern int check_repository_format_version(const char *var, const char *value, void *cb); extern int git_env_bool(const char *, int); @@ -1140,7 +1143,13 @@ extern const char *get_commit_output_encoding(void); extern int git_config_parse_parameter(const char *, config_fn_t fn, void *data); -extern const char *config_exclusive_filename; +struct config_include_data { + int depth; + config_fn_t fn; + void *data; +}; +#define CONFIG_INCLUDE_INIT { 0 } +extern int git_config_include(const char *name, const char *value, void *data); #define MAX_GITNAME (1000) extern char git_default_email[MAX_GITNAME]; @@ -26,7 +26,68 @@ static config_file *cf; static int zlib_compression_seen; -const char *config_exclusive_filename = NULL; +#define MAX_INCLUDE_DEPTH 10 +static const char include_depth_advice[] = +"exceeded maximum include depth (%d) while including\n" +" %s\n" +"from\n" +" %s\n" +"Do you have circular includes?"; +static int handle_path_include(const char *path, struct config_include_data *inc) +{ + int ret = 0; + struct strbuf buf = STRBUF_INIT; + + /* + * Use an absolute path as-is, but interpret relative paths + * based on the including config file. + */ + if (!is_absolute_path(path)) { + char *slash; + + if (!cf || !cf->name) + return error("relative config includes must come from files"); + + slash = find_last_dir_sep(cf->name); + if (slash) + strbuf_add(&buf, cf->name, slash - cf->name + 1); + strbuf_addstr(&buf, path); + path = buf.buf; + } + + if (!access(path, R_OK)) { + if (++inc->depth > MAX_INCLUDE_DEPTH) + die(include_depth_advice, MAX_INCLUDE_DEPTH, path, + cf && cf->name ? cf->name : "the command line"); + ret = git_config_from_file(git_config_include, path, inc); + inc->depth--; + } + strbuf_release(&buf); + return ret; +} + +int git_config_include(const char *var, const char *value, void *data) +{ + struct config_include_data *inc = data; + const char *type; + int ret; + + /* + * Pass along all values, including "include" directives; this makes it + * possible to query information on the includes themselves. + */ + ret = inc->fn(var, value, inc->data); + if (ret < 0) + return ret; + + type = skip_prefix(var, "include."); + if (!type) + return ret; + + if (!strcmp(type, "path")) + ret = handle_path_include(value, inc); + return ret; +} static void lowercase(char *p) { @@ -879,9 +940,6 @@ int git_config_early(config_fn_t fn, void *data, const char *repo_config) int ret = 0, found = 0; const char *home = NULL; - /* Setting $GIT_CONFIG makes git read _only_ the given config file. */ - if (config_exclusive_filename) - return git_config_from_file(fn, config_exclusive_filename, data); if (git_config_system() && !access(git_etc_gitconfig(), R_OK)) { ret += git_config_from_file(fn, git_etc_gitconfig(), data); @@ -917,10 +975,26 @@ int git_config_early(config_fn_t fn, void *data, const char *repo_config) return ret == 0 ? found : ret; } -int git_config(config_fn_t fn, void *data) +int git_config_with_options(config_fn_t fn, void *data, + const char *filename, int respect_includes) { char *repo_config = NULL; int ret; + struct config_include_data inc = CONFIG_INCLUDE_INIT; + + if (respect_includes) { + inc.fn = fn; + inc.data = data; + fn = git_config_include; + data = &inc; + } + + /* + * If we have a specific filename, use it. Otherwise, follow the + * regular lookup sequence. + */ + if (filename) + return git_config_from_file(fn, filename, data); repo_config = git_pathdup("config"); ret = git_config_early(fn, data, repo_config); @@ -929,6 +1003,11 @@ int git_config(config_fn_t fn, void *data) return ret; } +int git_config(config_fn_t fn, void *data) +{ + return git_config_with_options(fn, data, NULL, 1); +} + /* * Find all the stuff for git_config_set() below. */ @@ -1233,6 +1312,7 @@ int git_config_set_multivar_in_file(const char *config_filename, int fd = -1, in_fd; int ret; struct lock_file *lock = NULL; + char *filename_buf = NULL; /* parse-key returns negative; flip the sign to feed exit(3) */ ret = 0 - git_config_parse_key(key, &store.key, &store.baselen); @@ -1241,6 +1321,8 @@ int git_config_set_multivar_in_file(const char *config_filename, store.multi_replace = multi_replace; + if (!config_filename) + config_filename = filename_buf = git_pathdup("config"); /* * The lock serves a purpose in addition to locking: the new @@ -1410,6 +1492,7 @@ int git_config_set_multivar_in_file(const char *config_filename, out_free: if (lock) rollback_lock_file(lock); + free(filename_buf); return ret; write_err_out: @@ -1421,19 +1504,8 @@ write_err_out: int git_config_set_multivar(const char *key, const char *value, const char *value_regex, int multi_replace) { - const char *config_filename; - char *buf = NULL; - int ret; - - if (config_exclusive_filename) - config_filename = config_exclusive_filename; - else - config_filename = buf = git_pathdup("config"); - - ret = git_config_set_multivar_in_file(config_filename, key, value, - value_regex, multi_replace); - free(buf); - return ret; + return git_config_set_multivar_in_file(NULL, key, value, value_regex, + multi_replace); } static int section_name_match (const char *buf, const char *name) @@ -1476,19 +1548,19 @@ static int section_name_match (const char *buf, const char *name) } /* if new_name == NULL, the section is removed instead */ -int git_config_rename_section(const char *old_name, const char *new_name) +int git_config_rename_section_in_file(const char *config_filename, + const char *old_name, const char *new_name) { int ret = 0, remove = 0; - char *config_filename; + char *filename_buf = NULL; struct lock_file *lock = xcalloc(sizeof(struct lock_file), 1); int out_fd; char buf[1024]; FILE *config_file; - if (config_exclusive_filename) - config_filename = xstrdup(config_exclusive_filename); - else - config_filename = git_pathdup("config"); + if (!config_filename) + config_filename = filename_buf = git_pathdup("config"); + out_fd = hold_lock_file_for_update(lock, config_filename, 0); if (out_fd < 0) { ret = error("could not lock config file %s", config_filename); @@ -1552,10 +1624,15 @@ unlock_and_out: if (commit_lock_file(lock) < 0) ret = error("could not commit config file %s", config_filename); out: - free(config_filename); + free(filename_buf); return ret; } +int git_config_rename_section(const char *old_name, const char *new_name) +{ + return git_config_rename_section_in_file(NULL, old_name, new_name); +} + /* * Call this to report error for your variable that should not * get a boolean value (i.e. "[my] var" means "true"). diff --git a/t/t1300-repo-config.sh b/t/t1300-repo-config.sh index 0690e0edf4..5f249f681e 100755 --- a/t/t1300-repo-config.sh +++ b/t/t1300-repo-config.sh @@ -451,13 +451,21 @@ test_expect_success 'refer config from subdirectory' ' mkdir x && ( cd x && - echo strasse >expect + echo strasse >expect && git config --get --file ../other-config ein.bahn >actual && test_cmp expect actual ) ' +test_expect_success 'refer config from subdirectory via GIT_CONFIG' ' + ( + cd x && + GIT_CONFIG=../other-config git config --get ein.bahn >actual && + test_cmp expect actual + ) +' + cat > expect << EOF [ein] bahn = strasse @@ -960,4 +968,21 @@ test_expect_success 'git -c complains about empty key and value' ' test_must_fail git -c "" rev-parse ' +test_expect_success 'git config --edit works' ' + git config -f tmp test.value no && + echo test.value=yes >expect && + GIT_EDITOR="echo [test]value=yes >" git config -f tmp --edit && + git config -f tmp --list >actual && + test_cmp expect actual +' + +test_expect_success 'git config --edit respects core.editor' ' + git config -f tmp test.value no && + echo test.value=yes >expect && + test_config core.editor "echo [test]value=yes >" && + git config -f tmp --edit && + git config -f tmp --list >actual && + test_cmp expect actual +' + test_done diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh new file mode 100755 index 0000000000..4b1cbaa028 --- /dev/null +++ b/t/t1305-config-include.sh @@ -0,0 +1,134 @@ +#!/bin/sh + +test_description='test config file include directives' +. ./test-lib.sh + +test_expect_success 'include file by absolute path' ' + echo "[test]one = 1" >one && + echo "[include]path = \"$(pwd)/one\"" >.gitconfig && + echo 1 >expect && + git config test.one >actual && + test_cmp expect actual +' + +test_expect_success 'include file by relative path' ' + echo "[test]one = 1" >one && + echo "[include]path = one" >.gitconfig && + echo 1 >expect && + git config test.one >actual && + test_cmp expect actual +' + +test_expect_success 'chained relative paths' ' + mkdir subdir && + echo "[test]three = 3" >subdir/three && + echo "[include]path = three" >subdir/two && + echo "[include]path = subdir/two" >.gitconfig && + echo 3 >expect && + git config test.three >actual && + test_cmp expect actual +' + +test_expect_success 'include options can still be examined' ' + echo "[test]one = 1" >one && + echo "[include]path = one" >.gitconfig && + echo one >expect && + git config include.path >actual && + test_cmp expect actual +' + +test_expect_success 'listing includes option and expansion' ' + echo "[test]one = 1" >one && + echo "[include]path = one" >.gitconfig && + cat >expect <<-\EOF && + include.path=one + test.one=1 + EOF + git config --list >actual.full && + grep -v ^core actual.full >actual && + test_cmp expect actual +' + +test_expect_success 'single file lookup does not expand includes by default' ' + echo "[test]one = 1" >one && + echo "[include]path = one" >.gitconfig && + test_must_fail git config -f .gitconfig test.one && + test_must_fail git config --global test.one && + echo 1 >expect && + git config --includes -f .gitconfig test.one >actual && + test_cmp expect actual +' + +test_expect_success 'single file list does not expand includes by default' ' + echo "[test]one = 1" >one && + echo "[include]path = one" >.gitconfig && + echo "include.path=one" >expect && + git config -f .gitconfig --list >actual && + test_cmp expect actual +' + +test_expect_success 'writing config file does not expand includes' ' + echo "[test]one = 1" >one && + echo "[include]path = one" >.gitconfig && + git config test.two 2 && + echo 2 >expect && + git config --no-includes test.two >actual && + test_cmp expect actual && + test_must_fail git config --no-includes test.one +' + +test_expect_success 'config modification does not affect includes' ' + echo "[test]one = 1" >one && + echo "[include]path = one" >.gitconfig && + git config test.one 2 && + echo 1 >expect && + git config -f one test.one >actual && + test_cmp expect actual && + cat >expect <<-\EOF && + 1 + 2 + EOF + git config --get-all test.one >actual && + test_cmp expect actual +' + +test_expect_success 'missing include files are ignored' ' + cat >.gitconfig <<-\EOF && + [include]path = foo + [test]value = yes + EOF + echo yes >expect && + git config test.value >actual && + test_cmp expect actual +' + +test_expect_success 'absolute includes from command line work' ' + echo "[test]one = 1" >one && + echo 1 >expect && + git -c include.path="$PWD/one" config test.one >actual && + test_cmp expect actual +' + +test_expect_success 'relative includes from command line fail' ' + echo "[test]one = 1" >one && + test_must_fail git -c include.path=one config test.one +' + +test_expect_success 'include cycles are detected' ' + cat >.gitconfig <<-\EOF && + [test]value = gitconfig + [include]path = cycle + EOF + cat >cycle <<-\EOF && + [test]value = cycle + [include]path = .gitconfig + EOF + cat >expect <<-\EOF && + gitconfig + cycle + EOF + test_must_fail git config --get-all test.value 2>stderr && + grep "exceeded maximum include depth" stderr +' + +test_done |