config: add include directive

It can be useful to split your ~/.gitconfig across multiple files. For example, you might have a "main" file which is used on many machines, but a small set of per-machine tweaks. Or you may want to make some of your config public (e.g., clever aliases) while keeping other data back (e.g., your name or other identifying information). Or you may want to include a number of config options in some subset of your repos without copying and pasting (e.g., you want to reference them from the .git/config of participating repos). This patch introduces an include directive for config files. It looks like: [include] path = /path/to/file This is syntactically backwards-compatible with existing git config parsers (i.e., they will see it as another config entry and ignore it unless you are looking up include.path). The implementation provides a "git_config_include" callback which wraps regular config callbacks. Callers can pass it to git_config_from_file, and it will transparently follow any include directives, passing all of the discovered options to the real callback. Include directives are turned on automatically for "regular" git config parsing. This includes calls to git_config, as well as calls to the "git config" program that do not specify a single file (e.g., using "-f", "--global", etc). They are not turned on in other cases, including: 1. Parsing of other config-like files, like .gitmodules. There isn't a real need, and I'd rather be conservative and avoid unnecessary incompatibility or confusion. 2. Reading single files via "git config". This is for two reasons: a. backwards compatibility with scripts looking at config-like files. b. inspection of a specific file probably means you care about just what's in that file, not a general lookup for "do we have this value anywhere at all". If that is not the case, the caller can always specify "--includes". 3. Writing files via "git config"; we want to treat include.* variables as literal items to be copied (or modified), and not expand them. So "git config --unset-all foo.bar" would operate _only_ on .git/config, not any of its included files (just as it also does not operate on ~/.gitconfig). Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff King committed Feb 6, 2012 at 04:54 UTC 9b25a0b52e09400719366f0a33d0d0da98bbf7b0
7 files changed +292 -14
Documentation/config.txt
+15
@@ -84,6 +84,17 @@ customary UNIX fashion.
84
85 Some variables may require a special value format.
86
87 +Includes
88 +~~~~~~~~
89 +
90 +You can include one config file from another by setting the special
91 +`include.path` variable to the name of the file to be included. The
92 +included file is expanded immediately, as if its contents had been
93 +found at the location of the include directive. If the value of the
94 +`include.path` variable is a relative path, the path is considered to be
95 +relative to the configuration file in which the include directive was
96 +found. See below for examples.
97 +
98 Example
99 ~~~~~~~
100
@@ -106,6 +117,10 @@ Example
117 gitProxy="ssh" for "kernel.org"
118 gitProxy=default-proxy ; for the rest
119
120 + [include]
121 + path = /path/to/foo.inc ; include by absolute path
122 + path = foo ; expand "foo" relative to the current file
123 +
124 Variables
125 ~~~~~~~~~
126
Documentation/git-config.txt
+5
@@ -178,6 +178,11 @@ See also <<FILES>>.
178 Opens an editor to modify the specified config file; either
179 '--system', '--global', or repository (default).
180
181 +--includes::
182 +--no-includes::
183 + Respect `include.*` directives in config files when looking up
184 + values. Defaults to on.
185 +
186 [[FILES]]
187 FILES
188 -----
Documentation/technical/api-config.txt
+27 -1
@@ -52,13 +52,17 @@ while adjusting some of the default behavior of `git_config`. It should
52 almost never be used by "regular" git code that is looking up
53 configuration variables. It is intended for advanced callers like
54 `git-config`, which are intentionally tweaking the normal config-lookup
55 -process. It takes one extra parameter:
55 +process. It takes two extra parameters:
56
57 `filename`::
58 If this parameter is non-NULL, it specifies the name of a file to
59 parse for configuration, rather than looking in the usual files. Regular
60 `git_config` defaults to `NULL`.
61
62 +`respect_includes`::
63 +Specify whether include directives should be followed in parsed files.
64 +Regular `git_config` defaults to `1`.
65 +
66 There is a special version of `git_config` called `git_config_early`.
67 This version takes an additional parameter to specify the repository
68 config, instead of having it looked up via `git_path`. This is useful
@@ -108,6 +112,28 @@ string is given, prints an error message and returns -1.
112 Similar to `git_config_string`, but expands `~` or `~user` into the
113 user's home directory when found at the beginning of the path.
114
115 +Include Directives
116 +------------------
117 +
118 +By default, the config parser does not respect include directives.
119 +However, a caller can use the special `git_config_include` wrapper
120 +callback to support them. To do so, you simply wrap your "real" callback
121 +function and data pointer in a `struct config_include_data`, and pass
122 +the wrapper to the regular config-reading functions. For example:
123 +
124 +-------------------------------------------
125 +int read_file_with_include(const char *file, config_fn_t fn, void *data)
126 +{
127 + struct config_include_data inc = CONFIG_INCLUDE_INIT;
128 + inc.fn = fn;
129 + inc.data = data;
130 + return git_config_from_file(git_config_include, file, &inc);
131 +}
132 +-------------------------------------------
133 +
134 +`git_config` respects includes automatically. The lower-level
135 +`git_config_from_file` does not.
136 +
137 Writing Config Files
138 --------------------
139
builtin/config.c
+28 -10
@@ -25,6 +25,7 @@ static const char *given_config_file;
25 static int actions, types;
26 static const char *get_color_slot, *get_colorbool_slot;
27 static int end_null;
28 +static int respect_includes = -1;
29
30 #define ACTION_GET (1<<0)
31 #define ACTION_GET_ALL (1<<1)
@@ -74,6 +75,7 @@ static struct option builtin_config_options[] = {
75 OPT_BIT(0, "path", &types, "value is a path (file or directory name)", TYPE_PATH),
76 OPT_GROUP("Other"),
77 OPT_BOOLEAN('z', "null", &end_null, "terminate values with NUL byte"),
78 + OPT_BOOL(0, "includes", &respect_includes, "respect include directives on lookup"),
79 OPT_END(),
80 };
81
@@ -161,6 +163,9 @@ static int get_value(const char *key_, const char *regex_)
163 int ret = -1;
164 char *global = NULL, *repo_config = NULL;
165 const char *system_wide = NULL, *local;
166 + struct config_include_data inc = CONFIG_INCLUDE_INIT;
167 + config_fn_t fn;
168 + void *data;
169
170 local = given_config_file;
171 if (!local) {
@@ -213,19 +218,28 @@ static int get_value(const char *key_, const char *regex_)
218 }
219 }
220
221 + fn = show_config;
222 + data = NULL;
223 + if (respect_includes) {
224 + inc.fn = fn;
225 + inc.data = data;
226 + fn = git_config_include;
227 + data = &inc;
228 + }
229 +
230 if (do_all && system_wide)
217 - git_config_from_file(show_config, system_wide, NULL);
231 + git_config_from_file(fn, system_wide, data);
232 if (do_all && global)
219 - git_config_from_file(show_config, global, NULL);
233 + git_config_from_file(fn, global, data);
234 if (do_all)
221 - git_config_from_file(show_config, local, NULL);
222 - git_config_from_parameters(show_config, NULL);
235 + git_config_from_file(fn, local, data);
236 + git_config_from_parameters(fn, data);
237 if (!do_all && !seen)
224 - git_config_from_file(show_config, local, NULL);
238 + git_config_from_file(fn, local, data);
239 if (!do_all && !seen && global)
226 - git_config_from_file(show_config, global, NULL);
240 + git_config_from_file(fn, global, data);
241 if (!do_all && !seen && system_wide)
228 - git_config_from_file(show_config, system_wide, NULL);
242 + git_config_from_file(fn, system_wide, data);
243
244 free(key);
245 if (regexp) {
@@ -302,7 +316,7 @@ static void get_color(const char *def_color)
316 get_color_found = 0;
317 parsed_color[0] = '\0';
318 git_config_with_options(git_get_color_config, NULL,
305 - given_config_file);
319 + given_config_file, respect_includes);
320
321 if (!get_color_found && def_color)
322 color_parse(def_color, "command line", parsed_color);
@@ -330,7 +344,7 @@ static int get_colorbool(int print)
344 get_colorbool_found = -1;
345 get_diff_color_found = -1;
346 git_config_with_options(git_get_colorbool_config, NULL,
333 - given_config_file);
347 + given_config_file, respect_includes);
348
349 if (get_colorbool_found < 0) {
350 if (!strcmp(get_colorbool_slot, "color.diff"))
@@ -387,6 +401,9 @@ int cmd_config(int argc, const char **argv, const char *prefix)
401 given_config_file = given_config_file;
402 }
403
404 + if (respect_includes == -1)
405 + respect_includes = !given_config_file;
406 +
407 if (end_null) {
408 term = '\0';
409 delim = '\n';
@@ -424,7 +441,8 @@ int cmd_config(int argc, const char **argv, const char *prefix)
441 if (actions == ACTION_LIST) {
442 check_argc(argc, 0, 0);
443 if (git_config_with_options(show_all_config, NULL,
427 - given_config_file) < 0) {
444 + given_config_file,
445 + respect_includes) < 0) {
446 if (given_config_file)
447 die_errno("unable to read config file '%s'",
448 given_config_file);
cache.h
+10 -1
@@ -1113,7 +1113,8 @@ extern int git_config_from_file(config_fn_t fn, const char *, void *);
1113 extern void git_config_push_parameter(const char *text);
1114 extern int git_config_from_parameters(config_fn_t fn, void *data);
1115 extern int git_config(config_fn_t fn, void *);
1116 -extern int git_config_with_options(config_fn_t fn, void *, const char *filename);
1116 +extern int git_config_with_options(config_fn_t fn, void *,
1117 + const char *filename, int respect_includes);
1118 extern int git_config_early(config_fn_t fn, void *, const char *repo_config);
1119 extern int git_parse_ulong(const char *, unsigned long *);
1120 extern int git_config_int(const char *, const char *);
@@ -1140,6 +1141,14 @@ extern const char *get_commit_output_encoding(void);
1141
1142 extern int git_config_parse_parameter(const char *, config_fn_t fn, void *data);
1143
1144 +struct config_include_data {
1145 + int depth;
1146 + config_fn_t fn;
1147 + void *data;
1148 +};
1149 +#define CONFIG_INCLUDE_INIT { 0 }
1150 +extern int git_config_include(const char *name, const char *value, void *data);
1151 +
1152 #define MAX_GITNAME (1000)
1153 extern char git_default_email[MAX_GITNAME];
1154 extern char git_default_name[MAX_GITNAME];
config.c
+73 -2
@@ -26,6 +26,69 @@ static config_file *cf;
26
27 static int zlib_compression_seen;
28
29 +#define MAX_INCLUDE_DEPTH 10
30 +static const char include_depth_advice[] =
31 +"exceeded maximum include depth (%d) while including\n"
32 +" %s\n"
33 +"from\n"
34 +" %s\n"
35 +"Do you have circular includes?";
36 +static int handle_path_include(const char *path, struct config_include_data *inc)
37 +{
38 + int ret = 0;
39 + struct strbuf buf = STRBUF_INIT;
40 +
41 + /*
42 + * Use an absolute path as-is, but interpret relative paths
43 + * based on the including config file.
44 + */
45 + if (!is_absolute_path(path)) {
46 + char *slash;
47 +
48 + if (!cf || !cf->name)
49 + return error("relative config includes must come from files");
50 +
51 + slash = find_last_dir_sep(cf->name);
52 + if (slash)
53 + strbuf_add(&buf, cf->name, slash - cf->name + 1);
54 + strbuf_addstr(&buf, path);
55 + path = buf.buf;
56 + }
57 +
58 + if (!access(path, R_OK)) {
59 + if (++inc->depth > MAX_INCLUDE_DEPTH)
60 + die(include_depth_advice, MAX_INCLUDE_DEPTH, path,
61 + cf && cf->name ? cf->name : "the command line");
62 + ret = git_config_from_file(git_config_include, path, inc);
63 + inc->depth--;
64 + }
65 + strbuf_release(&buf);
66 + return ret;
67 +}
68 +
69 +int git_config_include(const char *var, const char *value, void *data)
70 +{
71 + struct config_include_data *inc = data;
72 + const char *type;
73 + int ret;
74 +
75 + /*
76 + * Pass along all values, including "include" directives; this makes it
77 + * possible to query information on the includes themselves.
78 + */
79 + ret = inc->fn(var, value, inc->data);
80 + if (ret < 0)
81 + return ret;
82 +
83 + type = skip_prefix(var, "include.");
84 + if (!type)
85 + return ret;
86 +
87 + if (!strcmp(type, "path"))
88 + ret = handle_path_include(value, inc);
89 + return ret;
90 +}
91 +
92 static void lowercase(char *p)
93 {
94 for (; *p; p++)
@@ -913,10 +976,18 @@ int git_config_early(config_fn_t fn, void *data, const char *repo_config)
976 }
977
978 int git_config_with_options(config_fn_t fn, void *data,
916 - const char *filename)
979 + const char *filename, int respect_includes)
980 {
981 char *repo_config = NULL;
982 int ret;
983 + struct config_include_data inc = CONFIG_INCLUDE_INIT;
984 +
985 + if (respect_includes) {
986 + inc.fn = fn;
987 + inc.data = data;
988 + fn = git_config_include;
989 + data = &inc;
990 + }
991
992 /*
993 * If we have a specific filename, use it. Otherwise, follow the
@@ -934,7 +1005,7 @@ int git_config_with_options(config_fn_t fn, void *data,
1005
1006 int git_config(config_fn_t fn, void *data)
1007 {
937 - return git_config_with_options(fn, data, NULL);
1008 + return git_config_with_options(fn, data, NULL, 1);
1009 }
1010
1011 /*
t/t1305-config-include.sh new
+134
@@ -0,0 +1,134 @@
1 +#!/bin/sh
2 +
3 +test_description='test config file include directives'
4 +. ./test-lib.sh
5 +
6 +test_expect_success 'include file by absolute path' '
7 + echo "[test]one = 1" >one &&
8 + echo "[include]path = \"$(pwd)/one\"" >.gitconfig &&
9 + echo 1 >expect &&
10 + git config test.one >actual &&
11 + test_cmp expect actual
12 +'
13 +
14 +test_expect_success 'include file by relative path' '
15 + echo "[test]one = 1" >one &&
16 + echo "[include]path = one" >.gitconfig &&
17 + echo 1 >expect &&
18 + git config test.one >actual &&
19 + test_cmp expect actual
20 +'
21 +
22 +test_expect_success 'chained relative paths' '
23 + mkdir subdir &&
24 + echo "[test]three = 3" >subdir/three &&
25 + echo "[include]path = three" >subdir/two &&
26 + echo "[include]path = subdir/two" >.gitconfig &&
27 + echo 3 >expect &&
28 + git config test.three >actual &&
29 + test_cmp expect actual
30 +'
31 +
32 +test_expect_success 'include options can still be examined' '
33 + echo "[test]one = 1" >one &&
34 + echo "[include]path = one" >.gitconfig &&
35 + echo one >expect &&
36 + git config include.path >actual &&
37 + test_cmp expect actual
38 +'
39 +
40 +test_expect_success 'listing includes option and expansion' '
41 + echo "[test]one = 1" >one &&
42 + echo "[include]path = one" >.gitconfig &&
43 + cat >expect <<-\EOF &&
44 + include.path=one
45 + test.one=1
46 + EOF
47 + git config --list >actual.full &&
48 + grep -v ^core actual.full >actual &&
49 + test_cmp expect actual
50 +'
51 +
52 +test_expect_success 'single file lookup does not expand includes by default' '
53 + echo "[test]one = 1" >one &&
54 + echo "[include]path = one" >.gitconfig &&
55 + test_must_fail git config -f .gitconfig test.one &&
56 + test_must_fail git config --global test.one &&
57 + echo 1 >expect &&
58 + git config --includes -f .gitconfig test.one >actual &&
59 + test_cmp expect actual
60 +'
61 +
62 +test_expect_success 'single file list does not expand includes by default' '
63 + echo "[test]one = 1" >one &&
64 + echo "[include]path = one" >.gitconfig &&
65 + echo "include.path=one" >expect &&
66 + git config -f .gitconfig --list >actual &&
67 + test_cmp expect actual
68 +'
69 +
70 +test_expect_success 'writing config file does not expand includes' '
71 + echo "[test]one = 1" >one &&
72 + echo "[include]path = one" >.gitconfig &&
73 + git config test.two 2 &&
74 + echo 2 >expect &&
75 + git config --no-includes test.two >actual &&
76 + test_cmp expect actual &&
77 + test_must_fail git config --no-includes test.one
78 +'
79 +
80 +test_expect_success 'config modification does not affect includes' '
81 + echo "[test]one = 1" >one &&
82 + echo "[include]path = one" >.gitconfig &&
83 + git config test.one 2 &&
84 + echo 1 >expect &&
85 + git config -f one test.one >actual &&
86 + test_cmp expect actual &&
87 + cat >expect <<-\EOF &&
88 + 1
89 + 2
90 + EOF
91 + git config --get-all test.one >actual &&
92 + test_cmp expect actual
93 +'
94 +
95 +test_expect_success 'missing include files are ignored' '
96 + cat >.gitconfig <<-\EOF &&
97 + [include]path = foo
98 + [test]value = yes
99 + EOF
100 + echo yes >expect &&
101 + git config test.value >actual &&
102 + test_cmp expect actual
103 +'
104 +
105 +test_expect_success 'absolute includes from command line work' '
106 + echo "[test]one = 1" >one &&
107 + echo 1 >expect &&
108 + git -c include.path="$PWD/one" config test.one >actual &&
109 + test_cmp expect actual
110 +'
111 +
112 +test_expect_success 'relative includes from command line fail' '
113 + echo "[test]one = 1" >one &&
114 + test_must_fail git -c include.path=one config test.one
115 +'
116 +
117 +test_expect_success 'include cycles are detected' '
118 + cat >.gitconfig <<-\EOF &&
119 + [test]value = gitconfig
120 + [include]path = cycle
121 + EOF
122 + cat >cycle <<-\EOF &&
123 + [test]value = cycle
124 + [include]path = .gitconfig
125 + EOF
126 + cat >expect <<-\EOF &&
127 + gitconfig
128 + cycle
129 + EOF
130 + test_must_fail git config --get-all test.value 2>stderr &&
131 + grep "exceeded maximum include depth" stderr
132 +'
133 +
134 +test_done