Merge branch 'jk/config-include'
authorJunio C Hamano <gitster@pobox.com>
Thu, 23 Feb 2012 21:30:14 +0000 (13:30 -0800)
committerJunio C Hamano <gitster@pobox.com>
Thu, 23 Feb 2012 21:30:14 +0000 (13:30 -0800)
* 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

Documentation/config.txt
Documentation/git-config.txt
Documentation/technical/api-config.txt [new file with mode: 0644]
builtin/config.c
cache.h
config.c
t/t1300-repo-config.sh
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
 -----
diff --git a/Documentation/technical/api-config.txt b/Documentation/technical/api-config.txt
new file mode 100644 (file)
index 0000000..edf8dfb
--- /dev/null
@@ -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
index d35c06ae51573eafbddd6309fda3f90ecef35d54..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,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)
diff --git a/cache.h b/cache.h
index 422c5cfcb374259b46a7f92fc2a08b726f54f77a..e12b15f4b92da906c50f7f51df95b126347e9441 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -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];
index 40f9c6d10317ed47f7786e5c328df3ab6f167e7c..ad0390819d2701d6153adf9db2947ee4908742ce 100644 (file)
--- a/config.c
+++ b/config.c
@@ -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 @@ int git_config_set_multivar_in_file(const char *config_filename,
 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 @@ int git_config_rename_section(const char *old_name, const char *new_name)
        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").
index 0690e0edf4e758200d4febb1c7837b5c7059add6..5f249f681e9324d7d35e3aee4dc9a834beff6c75 100755 (executable)
@@ -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 (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