config: learn the "onbranch:" includeIf condition

Currently, if a user wishes to have individual settings per branch, they are required to manually keep track of the settings in their head and manually set the options on the command-line or change the config at each branch. Teach config the "onbranch:" includeIf condition so that it can conditionally include configuration files if the branch that is checked out in the current worktree matches the pattern given. Signed-off-by: Denton Liu <liu.denton@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Denton Liu committed Jun 5, 2019 at 17:21 UTC 07b2c0eacac91c8db1a371667ed621cff443cf0d
3 files changed +87 -2
Documentation/config.txt
+19
@@ -144,6 +144,20 @@ refer to linkgit:gitignore[5] for details. For convenience:
144 This is the same as `gitdir` except that matching is done
145 case-insensitively (e.g. on case-insensitive file sytems)
146
147 +`onbranch`::
148 + The data that follows the keyword `onbranch:` is taken to be a
149 + pattern with standard globbing wildcards and two additional
150 + ones, `**/` and `/**`, that can match multiple path components.
151 + If we are in a worktree where the name of the branch that is
152 + currently checked out matches the pattern, the include condition
153 + is met.
154 ++
155 +If the pattern ends with `/`, `**` will be automatically added. For
156 +example, the pattern `foo/` becomes `foo/**`. In other words, it matches
157 +all branches that begin with `foo/`. This is useful if your branches are
158 +organized hierarchically and you would like to apply a configuration to
159 +all the branches in that hierarchy.
160 +
161 A few more notes on matching via `gitdir` and `gitdir/i`:
162
163 * Symlinks in `$GIT_DIR` are not resolved before matching.
@@ -206,6 +220,11 @@ Example
220 [includeIf "gitdir:/path/to/group/"]
221 path = foo.inc
222
223 + ; include only if we are in a worktree where foo-branch is
224 + ; currently checked out
225 + [includeIf "onbranch:foo-branch"]
226 + path = foo.inc
227 +
228 Values
229 ~~~~~~
230
config.c
+29 -2
@@ -19,6 +19,7 @@
19 #include "utf8.h"
20 #include "dir.h"
21 #include "color.h"
22 +#include "refs.h"
23
24 struct config_source {
25 struct config_source *prev;
@@ -170,6 +171,12 @@ static int handle_path_include(const char *path, struct config_include_data *inc
171 return ret;
172 }
173
174 +static void add_trailing_starstar_for_dir(struct strbuf *pat)
175 +{
176 + if (pat->len && is_dir_sep(pat->buf[pat->len - 1]))
177 + strbuf_addstr(pat, "**");
178 +}
179 +
180 static int prepare_include_condition_pattern(struct strbuf *pat)
181 {
182 struct strbuf path = STRBUF_INIT;
@@ -199,8 +206,7 @@ static int prepare_include_condition_pattern(struct strbuf *pat)
206 } else if (!is_absolute_path(pat->buf))
207 strbuf_insert(pat, 0, "**/", 3);
208
202 - if (pat->len && is_dir_sep(pat->buf[pat->len - 1]))
203 - strbuf_addstr(pat, "**");
209 + add_trailing_starstar_for_dir(pat);
210
211 strbuf_release(&path);
212 return prefix;
@@ -264,6 +270,25 @@ done:
270 return ret;
271 }
272
273 +static int include_by_branch(const char *cond, size_t cond_len)
274 +{
275 + int flags;
276 + int ret;
277 + struct strbuf pattern = STRBUF_INIT;
278 + const char *refname = resolve_ref_unsafe("HEAD", 0, NULL, &flags);
279 + const char *shortname;
280 +
281 + if (!refname || !(flags & REF_ISSYMREF) ||
282 + !skip_prefix(refname, "refs/heads/", &shortname))
283 + return 0;
284 +
285 + strbuf_add(&pattern, cond, cond_len);
286 + add_trailing_starstar_for_dir(&pattern);
287 + ret = !wildmatch(pattern.buf, shortname, WM_PATHNAME);
288 + strbuf_release(&pattern);
289 + return ret;
290 +}
291 +
292 static int include_condition_is_true(const struct config_options *opts,
293 const char *cond, size_t cond_len)
294 {
@@ -272,6 +297,8 @@ static int include_condition_is_true(const struct config_options *opts,
297 return include_by_gitdir(opts, cond, cond_len, 0);
298 else if (skip_prefix_mem(cond, cond_len, "gitdir/i:", &cond, &cond_len))
299 return include_by_gitdir(opts, cond, cond_len, 1);
300 + else if (skip_prefix_mem(cond, cond_len, "onbranch:", &cond, &cond_len))
301 + return include_by_branch(cond, cond_len);
302
303 /* unknown conditionals are always false */
304 return 0;
t/t1305-config-include.sh
+39
@@ -309,6 +309,45 @@ test_expect_success SYMLINKS 'conditional include, gitdir matching symlink, icas
309 )
310 '
311
312 +test_expect_success 'conditional include, onbranch' '
313 + echo "[includeIf \"onbranch:foo-branch\"]path=bar9" >>.git/config &&
314 + echo "[test]nine=9" >.git/bar9 &&
315 + git checkout -b master &&
316 + test_must_fail git config test.nine &&
317 + git checkout -b foo-branch &&
318 + echo 9 >expect &&
319 + git config test.nine >actual &&
320 + test_cmp expect actual
321 +'
322 +
323 +test_expect_success 'conditional include, onbranch, wildcard' '
324 + echo "[includeIf \"onbranch:?oo-*/**\"]path=bar10" >>.git/config &&
325 + echo "[test]ten=10" >.git/bar10 &&
326 + git checkout -b not-foo-branch/a &&
327 + test_must_fail git config test.ten &&
328 +
329 + echo 10 >expect &&
330 + git checkout -b foo-branch/a/b/c &&
331 + git config test.ten >actual &&
332 + test_cmp expect actual &&
333 +
334 + git checkout -b moo-bar/a &&
335 + git config test.ten >actual &&
336 + test_cmp expect actual
337 +'
338 +
339 +test_expect_success 'conditional include, onbranch, implicit /** for /' '
340 + echo "[includeIf \"onbranch:foo-dir/\"]path=bar11" >>.git/config &&
341 + echo "[test]eleven=11" >.git/bar11 &&
342 + git checkout -b not-foo-dir/a &&
343 + test_must_fail git config test.eleven &&
344 +
345 + echo 11 >expect &&
346 + git checkout -b foo-dir/a/b/c &&
347 + git config test.eleven >actual &&
348 + test_cmp expect actual
349 +'
350 +
351 test_expect_success 'include cycles are detected' '
352 cat >.gitconfig <<-\EOF &&
353 [test]value = gitconfig