config: add include directive
authorJeff King <peff@peff.net>
Mon, 6 Feb 2012 09:54:04 +0000 (04:54 -0500)
committerJunio C Hamano <gitster@pobox.com>
Fri, 17 Feb 2012 15:59:55 +0000 (07:59 -0800)
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>
Documentation/config.txt
Documentation/git-config.txt
Documentation/technical/api-config.txt
builtin/config.c
cache.h
config.c
t/t1305-config-include.sh [new file with mode: 0755]
index abeb82b2c6d40e8557f7a5f8ad4c5e98b3a26a62..e55dae1806a8889d8179c94139bb60a2c5f7a9a6 100644 (file)
@@ -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
 ~~~~~~~~~
 
index e7ecf5d803e14dfa452671cf01e7730dce48b984..aa8303b1adb1ac6efba6a5919a6a59495e89c6fb 100644 (file)
@@ -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
 -----
index b0aeb2e481ecabdb4ce60dce4aa8efe61f06f648..edf8dfb99b0accf1c3cbe870bdc332783a344dc9 100644 (file)
@@ -52,13 +52,17 @@ 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 one extra parameter:
+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
@@ -108,6 +112,28 @@ string is given, prints an error message and returns -1.
 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
 --------------------
 
index ccbb13add2b80e39ef806d352850d85e1f73f819..d41a9bfb143c2bd82e539c3f390f17914c2e853a 100644 (file)
@@ -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,6 +163,9 @@ 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 = given_config_file;
        if (!local) {
@@ -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) {
@@ -302,7 +316,7 @@ static void get_color(const char *def_color)
        get_color_found = 0;
        parsed_color[0] = '\0';
        git_config_with_options(git_get_color_config, NULL,
-                               given_config_file);
+                               given_config_file, respect_includes);
 
        if (!get_color_found && def_color)
                color_parse(def_color, "command line", parsed_color);
@@ -330,7 +344,7 @@ static int get_colorbool(int print)
        get_colorbool_found = -1;
        get_diff_color_found = -1;
        git_config_with_options(git_get_colorbool_config, NULL,
-                               given_config_file);
+                               given_config_file, respect_includes);
 
        if (get_colorbool_found < 0) {
                if (!strcmp(get_colorbool_slot, "color.diff"))
@@ -387,6 +401,9 @@ int cmd_config(int argc, const char **argv, const char *prefix)
                        given_config_file = given_config_file;
        }
 
+       if (respect_includes == -1)
+               respect_includes = !given_config_file;
+
        if (end_null) {
                term = '\0';
                delim = '\n';
@@ -424,7 +441,8 @@ int cmd_config(int argc, const char **argv, const char *prefix)
        if (actions == ACTION_LIST) {
                check_argc(argc, 0, 0);
                if (git_config_with_options(show_all_config, NULL,
-                                           given_config_file) < 0) {
+                                           given_config_file,
+                                           respect_includes) < 0) {
                        if (given_config_file)
                                die_errno("unable to read config file '%s'",
                                          given_config_file);
diff --git a/cache.h b/cache.h
index 52d2c1e5844ea7354daee81cbb150580f227da35..8fdad9412d8fc7cd399b0c342a93ddb9f1a4672f 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -1113,7 +1113,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);
+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 *);
@@ -1140,6 +1141,14 @@ extern const char *get_commit_output_encoding(void);
 
 extern int git_config_parse_parameter(const char *, config_fn_t fn, void *data);
 
+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];
 extern char git_default_name[MAX_GITNAME];
index 1e30ad9d18dc51eeaee72e33b6cc133e6ead1c99..ad0390819d2701d6153adf9db2947ee4908742ce 100644 (file)
--- a/config.c
+++ b/config.c
@@ -26,6 +26,69 @@ static config_file *cf;
 
 static int zlib_compression_seen;
 
+#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)
 {
        for (; *p; p++)
@@ -913,10 +976,18 @@ int git_config_early(config_fn_t fn, void *data, const char *repo_config)
 }
 
 int git_config_with_options(config_fn_t fn, void *data,
-                           const char *filename)
+                           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
@@ -934,7 +1005,7 @@ int git_config_with_options(config_fn_t fn, void *data,
 
 int git_config(config_fn_t fn, void *data)
 {
-       return git_config_with_options(fn, data, NULL);
+       return git_config_with_options(fn, data, NULL, 1);
 }
 
 /*
diff --git a/t/t1305-config-include.sh b/t/t1305-config-include.sh
new file mode 100755 (executable)
index 0000000..4b1cbaa
--- /dev/null
@@ -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