]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'jt/conditional-config-on-remote-url'
authorJunio C Hamano <gitster@pobox.com>
Wed, 9 Feb 2022 22:20:58 +0000 (14:20 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 9 Feb 2022 22:20:59 +0000 (14:20 -0800)
The conditional inclusion mechanism of configuration files using
"[includeIf <condition>]" learns to base its decision on the
URL of the remote repository the repository interacts with.

* jt/conditional-config-on-remote-url:
  config: include file if remote URL matches a glob
  config: make git_config_include() static

1  2 
Documentation/config.txt
config.c
t/t1300-config.sh

diff --combined Documentation/config.txt
index b168f02dc3d92c77be4d37061403a2909e6235c8,0b8929baa4784f84adead8e638380e6ce302a1d3..bf3e512921fe75744ead531d3fa8252aa7039885
@@@ -159,6 -159,33 +159,33 @@@ all branches that begin with `foo/`. Th
  organized hierarchically and you would like to apply a configuration to
  all the branches in that hierarchy.
  
+ `hasconfig:remote.*.url:`::
+       The data that follows this keyword is taken to
+       be a pattern with standard globbing wildcards and two
+       additional ones, `**/` and `/**`, that can match multiple
+       components. The first time this keyword is seen, the rest of
+       the config files will be scanned for remote URLs (without
+       applying any values). If there exists at least one remote URL
+       that matches this pattern, the include condition is met.
+ +
+ Files included by this option (directly or indirectly) are not allowed
+ to contain remote URLs.
+ +
+ Note that unlike other includeIf conditions, resolving this condition
+ relies on information that is not yet known at the point of reading the
+ condition. A typical use case is this option being present as a
+ system-level or global-level config, and the remote URL being in a
+ local-level config; hence the need to scan ahead when resolving this
+ condition. In order to avoid the chicken-and-egg problem in which
+ potentially-included files can affect whether such files are potentially
+ included, Git breaks the cycle by prohibiting these files from affecting
+ the resolution of these conditions (thus, prohibiting them from
+ declaring remote URLs).
+ +
+ As for the naming of this keyword, it is for forwards compatibiliy with
+ a naming scheme that supports more variable-based include conditions,
+ but currently Git only supports the exact keyword described above.
  A few more notes on matching via `gitdir` and `gitdir/i`:
  
   * Symlinks in `$GIT_DIR` are not resolved before matching.
@@@ -226,6 -253,14 +253,14 @@@ Exampl
  ; currently checked out
  [includeIf "onbranch:foo-branch"]
        path = foo.inc
+ ; include only if a remote with the given URL exists (note
+ ; that such a URL may be provided later in a file or in a
+ ; file read after this file is read, as seen in this example)
+ [includeIf "hasconfig:remote.*.url:https://example.com/**"]
+       path = foo.inc
+ [remote "origin"]
+       url = https://example.com/git
  ----
  
  Values
@@@ -262,19 -297,11 +297,19 @@@ color:
         colors (at most two, one for foreground and one for background)
         and attributes (as many as you want), separated by spaces.
  +
 -The basic colors accepted are `normal`, `black`, `red`, `green`, `yellow`,
 -`blue`, `magenta`, `cyan` and `white`.  The first color given is the
 -foreground; the second is the background.  All the basic colors except
 -`normal` have a bright variant that can be specified by prefixing the
 -color with `bright`, like `brightred`.
 +The basic colors accepted are `normal`, `black`, `red`, `green`,
 +`yellow`, `blue`, `magenta`, `cyan`, `white` and `default`.  The first
 +color given is the foreground; the second is the background.  All the
 +basic colors except `normal` and `default` have a bright variant that can
 +be specified by prefixing the color with `bright`, like `brightred`.
 ++
 +The color `normal` makes no change to the color. It is the same as an
 +empty string, but can be used as the foreground color when specifying a
 +background color alone (for example, "normal red").
 ++
 +The color `default` explicitly resets the color to the terminal default,
 +for example to specify a cleared background. Although it varies between
 +terminals, this is usually not the same as setting to "white black".
  +
  Colors may also be given as numbers between 0 and 255; these use ANSI
  256-color mode (but note that not all terminals may support this).  If
@@@ -288,11 -315,6 +323,11 @@@ The position of any attributes with res
  be turned off by prefixing them with `no` or `no-` (e.g., `noreverse`,
  `no-ul`, etc).
  +
 +The pseudo-attribute `reset` resets all colors and attributes before
 +applying the specified coloring. For example, `reset green` will result
 +in a green foreground and default background without any active
 +attributes.
 ++
  An empty color string produces no color effect at all. This can be used
  to avoid coloring specific elements without disabling color entirely.
  +
diff --combined config.c
index 2bffa8d4a01ba1f281d6e6fd95f35bf133cbd9c5,d98e6c9dc0ef33093742de03986c04ccc9fcdfd0..e0c03d154c916225d30fd87d7008c91582ac4409
+++ b/config.c
@@@ -120,6 -120,22 +120,22 @@@ static long config_buf_ftell(struct con
        return conf->u.buf.pos;
  }
  
+ struct config_include_data {
+       int depth;
+       config_fn_t fn;
+       void *data;
+       const struct config_options *opts;
+       struct git_config_source *config_source;
+       /*
+        * All remote URLs discovered when reading all config files.
+        */
+       struct string_list *remote_urls;
+ };
+ #define CONFIG_INCLUDE_INIT { 0 }
+ static int git_config_include(const char *var, const char *value, void *data);
  #define MAX_INCLUDE_DEPTH 10
  static const char include_depth_advice[] = N_(
  "exceeded maximum include depth (%d) while including\n"
@@@ -294,9 -310,92 +310,92 @@@ static int include_by_branch(const cha
        return ret;
  }
  
- static int include_condition_is_true(const struct config_options *opts,
+ static int add_remote_url(const char *var, const char *value, void *data)
+ {
+       struct string_list *remote_urls = data;
+       const char *remote_name;
+       size_t remote_name_len;
+       const char *key;
+       if (!parse_config_key(var, "remote", &remote_name, &remote_name_len,
+                             &key) &&
+           remote_name &&
+           !strcmp(key, "url"))
+               string_list_append(remote_urls, value);
+       return 0;
+ }
+ static void populate_remote_urls(struct config_include_data *inc)
+ {
+       struct config_options opts;
+       struct config_source *store_cf = cf;
+       struct key_value_info *store_kvi = current_config_kvi;
+       enum config_scope store_scope = current_parsing_scope;
+       opts = *inc->opts;
+       opts.unconditional_remote_url = 1;
+       cf = NULL;
+       current_config_kvi = NULL;
+       current_parsing_scope = 0;
+       inc->remote_urls = xmalloc(sizeof(*inc->remote_urls));
+       string_list_init_dup(inc->remote_urls);
+       config_with_options(add_remote_url, inc->remote_urls, inc->config_source, &opts);
+       cf = store_cf;
+       current_config_kvi = store_kvi;
+       current_parsing_scope = store_scope;
+ }
+ static int forbid_remote_url(const char *var, const char *value, void *data)
+ {
+       const char *remote_name;
+       size_t remote_name_len;
+       const char *key;
+       if (!parse_config_key(var, "remote", &remote_name, &remote_name_len,
+                             &key) &&
+           remote_name &&
+           !strcmp(key, "url"))
+               die(_("remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url"));
+       return 0;
+ }
+ static int at_least_one_url_matches_glob(const char *glob, int glob_len,
+                                        struct string_list *remote_urls)
+ {
+       struct strbuf pattern = STRBUF_INIT;
+       struct string_list_item *url_item;
+       int found = 0;
+       strbuf_add(&pattern, glob, glob_len);
+       for_each_string_list_item(url_item, remote_urls) {
+               if (!wildmatch(pattern.buf, url_item->string, WM_PATHNAME)) {
+                       found = 1;
+                       break;
+               }
+       }
+       strbuf_release(&pattern);
+       return found;
+ }
+ static int include_by_remote_url(struct config_include_data *inc,
+               const char *cond, size_t cond_len)
+ {
+       if (inc->opts->unconditional_remote_url)
+               return 1;
+       if (!inc->remote_urls)
+               populate_remote_urls(inc);
+       return at_least_one_url_matches_glob(cond, cond_len,
+                                            inc->remote_urls);
+ }
+ static int include_condition_is_true(struct config_include_data *inc,
                                     const char *cond, size_t cond_len)
  {
+       const struct config_options *opts = inc->opts;
  
        if (skip_prefix_mem(cond, cond_len, "gitdir:", &cond, &cond_len))
                return include_by_gitdir(opts, cond, cond_len, 0);
                return include_by_gitdir(opts, cond, cond_len, 1);
        else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
                return include_by_branch(cond, cond_len);
+       else if (skip_prefix_mem(cond, cond_len, "hasconfig:remote.*.url:", &cond,
+                                  &cond_len))
+               return include_by_remote_url(inc, cond, cond_len);
  
        /* unknown conditionals are always false */
        return 0;
  }
  
- int git_config_include(const char *var, const char *value, void *data)
static int git_config_include(const char *var, const char *value, void *data)
  {
        struct config_include_data *inc = data;
        const char *cond, *key;
                ret = handle_path_include(value, inc);
  
        if (!parse_config_key(var, "includeif", &cond, &cond_len, &key) &&
-           (cond && include_condition_is_true(inc->opts, cond, cond_len)) &&
-           !strcmp(key, "path"))
+           cond && include_condition_is_true(inc, cond, cond_len) &&
+           !strcmp(key, "path")) {
+               config_fn_t old_fn = inc->fn;
+               if (inc->opts->unconditional_remote_url)
+                       inc->fn = forbid_remote_url;
                ret = handle_path_include(value, inc);
+               inc->fn = old_fn;
+       }
  
        return ret;
  }
@@@ -1559,12 -1667,9 +1667,12 @@@ static int git_default_i18n_config(cons
  static int git_default_branch_config(const char *var, const char *value)
  {
        if (!strcmp(var, "branch.autosetupmerge")) {
 -              if (value && !strcasecmp(value, "always")) {
 +              if (value && !strcmp(value, "always")) {
                        git_branch_track = BRANCH_TRACK_ALWAYS;
                        return 0;
 +              } else if (value && !strcmp(value, "inherit")) {
 +                      git_branch_track = BRANCH_TRACK_INHERIT;
 +                      return 0;
                }
                git_branch_track = git_config_bool(var, value);
                return 0;
@@@ -1929,11 -2034,13 +2037,13 @@@ int config_with_options(config_fn_t fn
                        const struct config_options *opts)
  {
        struct config_include_data inc = CONFIG_INCLUDE_INIT;
+       int ret;
  
        if (opts->respect_includes) {
                inc.fn = fn;
                inc.data = data;
                inc.opts = opts;
+               inc.config_source = config_source;
                fn = git_config_include;
                data = &inc;
        }
         * regular lookup sequence.
         */
        if (config_source && config_source->use_stdin) {
-               return git_config_from_stdin(fn, data);
+               ret = git_config_from_stdin(fn, data);
        } else if (config_source && config_source->file) {
-               return git_config_from_file(fn, config_source->file, data);
+               ret = git_config_from_file(fn, config_source->file, data);
        } else if (config_source && config_source->blob) {
                struct repository *repo = config_source->repo ?
                        config_source->repo : the_repository;
-               return git_config_from_blob_ref(fn, repo, config_source->blob,
+               ret = git_config_from_blob_ref(fn, repo, config_source->blob,
                                                data);
+       } else {
+               ret = do_git_config_sequence(opts, fn, data);
        }
  
-       return do_git_config_sequence(opts, fn, data);
+       if (inc.remote_urls) {
+               string_list_clear(inc.remote_urls, 0);
+               FREE_AND_NULL(inc.remote_urls);
+       }
+       return ret;
  }
  
  static void configset_iter(struct config_set *cs, config_fn_t fn, void *data)
@@@ -2558,12 -2671,11 +2674,12 @@@ void git_die_config(const char *key, co
  {
        const struct string_list *values;
        struct key_value_info *kv_info;
 +      report_fn error_fn = get_error_routine();
  
        if (err) {
                va_list params;
                va_start(params, err);
 -              vreportf("error: ", err, params);
 +              error_fn(err, params);
                va_end(params);
        }
        values = git_config_get_value_multi(key);
diff --combined t/t1300-config.sh
index 78359f1f4a2d736f44b84075013e647a7fab3660,c6b5911c4d15cfa3b6297af81c35fb91577a36bd..7dd9b325d90fd1151f5a4202cad60d8a72df7780
@@@ -8,7 -8,6 +8,7 @@@ test_description='Test git config in di
  GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
  export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
  
 +TEST_PASSES_SANITIZE_LEAK=true
  . ./test-lib.sh
  
  test_expect_success 'clear default config' '
@@@ -718,8 -717,8 +718,8 @@@ test_expect_success bool 
        rm -f result &&
        for i in 1 2 3 4
        do
 -          git config --bool --get bool.true$i >>result
 -          git config --bool --get bool.false$i >>result
 +          git config --bool --get bool.true$i >>result &&
 +          git config --bool --get bool.false$i >>result || return 1
        done &&
        test_cmp expect result'
  
@@@ -902,7 -901,7 +902,7 @@@ test_expect_success 'get --expiry-date
        EOF
        : "work around heredoc parsing bug fixed in dash 0.5.7 (in ec2c84d)" &&
        {
 -              echo "$rel_out $(git config --expiry-date date.valid1)"
 +              echo "$rel_out $(git config --expiry-date date.valid1)" &&
                git config --expiry-date date.valid2 &&
                git config --expiry-date date.valid3 &&
                git config --expiry-date date.valid4 &&
@@@ -2388,4 -2387,122 +2388,122 @@@ test_expect_success '--get and --get-al
        test_must_fail git config --file=config --get-regexp --fixed-value fixed+ non-existent
  '
  
+ test_expect_success 'includeIf.hasconfig:remote.*.url' '
+       git init hasremoteurlTest &&
+       test_when_finished "rm -rf hasremoteurlTest" &&
+       cat >include-this <<-\EOF &&
+       [user]
+               this = this-is-included
+       EOF
+       cat >dont-include-that <<-\EOF &&
+       [user]
+               that = that-is-not-included
+       EOF
+       cat >>hasremoteurlTest/.git/config <<-EOF &&
+       [includeIf "hasconfig:remote.*.url:foourl"]
+               path = "$(pwd)/include-this"
+       [includeIf "hasconfig:remote.*.url:barurl"]
+               path = "$(pwd)/dont-include-that"
+       [remote "foo"]
+               url = foourl
+       EOF
+       echo this-is-included >expect-this &&
+       git -C hasremoteurlTest config --get user.this >actual-this &&
+       test_cmp expect-this actual-this &&
+       test_must_fail git -C hasremoteurlTest config --get user.that
+ '
+ test_expect_success 'includeIf.hasconfig:remote.*.url respects last-config-wins' '
+       git init hasremoteurlTest &&
+       test_when_finished "rm -rf hasremoteurlTest" &&
+       cat >include-two-three <<-\EOF &&
+       [user]
+               two = included-config
+               three = included-config
+       EOF
+       cat >>hasremoteurlTest/.git/config <<-EOF &&
+       [remote "foo"]
+               url = foourl
+       [user]
+               one = main-config
+               two = main-config
+       [includeIf "hasconfig:remote.*.url:foourl"]
+               path = "$(pwd)/include-two-three"
+       [user]
+               three = main-config
+       EOF
+       echo main-config >expect-main-config &&
+       echo included-config >expect-included-config &&
+       git -C hasremoteurlTest config --get user.one >actual &&
+       test_cmp expect-main-config actual &&
+       git -C hasremoteurlTest config --get user.two >actual &&
+       test_cmp expect-included-config actual &&
+       git -C hasremoteurlTest config --get user.three >actual &&
+       test_cmp expect-main-config actual
+ '
+ test_expect_success 'includeIf.hasconfig:remote.*.url globs' '
+       git init hasremoteurlTest &&
+       test_when_finished "rm -rf hasremoteurlTest" &&
+       printf "[user]\ndss = yes\n" >double-star-start &&
+       printf "[user]\ndse = yes\n" >double-star-end &&
+       printf "[user]\ndsm = yes\n" >double-star-middle &&
+       printf "[user]\nssm = yes\n" >single-star-middle &&
+       printf "[user]\nno = no\n" >no &&
+       cat >>hasremoteurlTest/.git/config <<-EOF &&
+       [remote "foo"]
+               url = https://foo/bar/baz
+       [includeIf "hasconfig:remote.*.url:**/baz"]
+               path = "$(pwd)/double-star-start"
+       [includeIf "hasconfig:remote.*.url:**/nomatch"]
+               path = "$(pwd)/no"
+       [includeIf "hasconfig:remote.*.url:https:/**"]
+               path = "$(pwd)/double-star-end"
+       [includeIf "hasconfig:remote.*.url:nomatch:/**"]
+               path = "$(pwd)/no"
+       [includeIf "hasconfig:remote.*.url:https:/**/baz"]
+               path = "$(pwd)/double-star-middle"
+       [includeIf "hasconfig:remote.*.url:https:/**/nomatch"]
+               path = "$(pwd)/no"
+       [includeIf "hasconfig:remote.*.url:https://*/bar/baz"]
+               path = "$(pwd)/single-star-middle"
+       [includeIf "hasconfig:remote.*.url:https://*/baz"]
+               path = "$(pwd)/no"
+       EOF
+       git -C hasremoteurlTest config --get user.dss &&
+       git -C hasremoteurlTest config --get user.dse &&
+       git -C hasremoteurlTest config --get user.dsm &&
+       git -C hasremoteurlTest config --get user.ssm &&
+       test_must_fail git -C hasremoteurlTest config --get user.no
+ '
+ test_expect_success 'includeIf.hasconfig:remote.*.url forbids remote url in such included files' '
+       git init hasremoteurlTest &&
+       test_when_finished "rm -rf hasremoteurlTest" &&
+       cat >include-with-url <<-\EOF &&
+       [remote "bar"]
+               url = barurl
+       EOF
+       cat >>hasremoteurlTest/.git/config <<-EOF &&
+       [includeIf "hasconfig:remote.*.url:foourl"]
+               path = "$(pwd)/include-with-url"
+       EOF
+       # test with any Git command
+       test_must_fail git -C hasremoteurlTest status 2>err &&
+       grep "fatal: remote URLs cannot be configured in file directly or indirectly included by includeIf.hasconfig:remote.*.url" err
+ '
  test_done