builtin/config: introduce "set" subcommand

Introduce a new "set" subcommand to git-config(1). Please refer to preceding commits regarding the motivation behind this change. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed May 6, 2024 at 10:56 UTC 00bbdde141f5bff378281d47c5c62ca06687b807
3 files changed +140 -63
Documentation/git-config.txt
+24 -14
@@ -11,9 +11,7 @@ SYNOPSIS
11 [verse]
12 'git config list' [<file-option>] [<display-option>] [--includes]
13 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
14 -'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
15 -'git config' [<file-option>] [--type=<type>] [--comment=<message>] --add <name> <value>
16 -'git config' [<file-option>] [--type=<type>] [--comment=<message>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
14 +'git config set' [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>
15 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
16 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
17 'git config' [<file-option>] --rename-section <old-name> <new-name>
@@ -27,7 +25,7 @@ You can query/set/replace/unset options with this command. The name is
25 actually the section and the key separated by a dot, and the value will be
26 escaped.
27
30 -Multiple lines can be added to an option by using the `--add` option.
28 +Multiple lines can be added to an option by using the `--append` option.
29 If you want to update or unset an option which can occur on multiple
30 lines, a `value-pattern` (which is an extended regular expression,
31 unless the `--fixed-value` option is given) needs to be given. Only the
@@ -82,6 +80,13 @@ get::
80 emits all values associated with key. Returns error code 1 if key is
81 not present.
82
83 +set::
84 + Set value for one or more config options. By default, this command
85 + refuses to write multi-valued config options. Passing `--all` will
86 + replace all multi-valued config options with the new value, whereas
87 + `--value=` will replace all config options whose values match the given
88 + pattern.
89 +
90 [[OPTIONS]]
91 OPTIONS
92 -------
@@ -90,10 +95,9 @@ OPTIONS
95 Default behavior is to replace at most one line. This replaces
96 all lines matching the key (and optionally the `value-pattern`).
97
93 ---add::
98 +--append::
99 Adds a new line to the option without altering any existing
95 - values. This is the same as providing '^$' as the `value-pattern`
96 - in `--replace-all`.
100 + values. This is the same as providing '--value=^$' in `set`.
101
102 --comment <message>::
103 Append a comment at the end of new or modified lines.
@@ -296,6 +300,9 @@ recommended to migrate to the new syntax.
300 'git config <name>'::
301 Replaced by `git config get <name>`.
302
303 +'git config <name> <value> [<value-pattern>]'::
304 + Replaced by `git config set [--value=<pattern>] <name> <value>`.
305 +
306 -l::
307 --list::
308 Replaced by `git config list`.
@@ -315,6 +322,9 @@ recommended to migrate to the new syntax.
322 --get-color <name> [<default>]::
323 Replaced by `git config get --type=color [--default=<default>] <name>`.
324
325 +--add <name> <value>::
326 + Replaced by `git config set --append <name> <value>`.
327 +
328 CONFIGURATION
329 -------------
330 `pager.config` is only respected when listing configuration, i.e., when
@@ -361,7 +371,7 @@ precedence over values read earlier. When multiple values are taken then all
371 values of a key from all files will be used.
372
373 By default, options are only written to the repository specific
364 -configuration file. Note that this also affects options like `--replace-all`
374 +configuration file. Note that this also affects options like `set`
375 and `--unset`. *'git config' will only ever change one file at a time*.
376
377 You can limit which configuration sources are read from or written to by
@@ -497,7 +507,7 @@ Given a .git/config like this:
507 you can set the filemode to true with
508
509 ------------
500 -% git config core.filemode true
510 +% git config set core.filemode true
511 ------------
512
513 The hypothetical proxy command entries actually have a postfix to discern
@@ -505,7 +515,7 @@ what URL they apply to. Here is how to change the entry for kernel.org
515 to "ssh".
516
517 ------------
508 -% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
518 +% git config set --value='for kernel.org$' core.gitproxy '"ssh" for kernel.org'
519 ------------
520
521 This makes sure that only the key/value pair for kernel.org is replaced.
@@ -541,26 +551,26 @@ If you like to live dangerously, you can replace *all* core.gitproxy by a
551 new one with
552
553 ------------
544 -% git config --replace-all core.gitproxy ssh
554 +% git config set --all core.gitproxy ssh
555 ------------
556
557 However, if you really only want to replace the line for the default proxy,
558 i.e. the one without a "for ..." postfix, do something like this:
559
560 ------------
551 -% git config core.gitproxy ssh '! for '
561 +% git config set --value='! for ' core.gitproxy ssh
562 ------------
563
564 To actually match only values with an exclamation mark, you have to
565
566 ------------
557 -% git config section.key value '[!]'
567 +% git config set --value='[!]' section.key value
568 ------------
569
570 To add a new proxy, without altering any of the existing ones, use
571
572 ------------
563 -% git config --add core.gitproxy '"proxy-command" for example.com'
573 +% git config set --append core.gitproxy '"proxy-command" for example.com'
574 ------------
575
576 An example to use customized color from the configuration in your
builtin/config.c
+63
@@ -18,6 +18,7 @@
18 static const char *const builtin_config_usage[] = {
19 N_("git config list [<file-option>] [<display-option>] [--includes]"),
20 N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
21 + N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
22 NULL
23 };
24
@@ -31,6 +32,11 @@ static const char *const builtin_config_get_usage[] = {
32 NULL
33 };
34
35 +static const char *const builtin_config_set_usage[] = {
36 + N_("git config set [<file-option>] [--type=<type>] [--comment=<message>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
37 + NULL
38 +};
39 +
40 static char *key;
41 static regex_t *key_regexp;
42 static const char *value_pattern;
@@ -849,9 +855,66 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix)
855 return get_value(argv[0], value_pattern, flags);
856 }
857
858 +static int cmd_config_set(int argc, const char **argv, const char *prefix)
859 +{
860 + const char *value_pattern = NULL, *comment_arg = NULL;
861 + char *comment = NULL;
862 + int flags = 0, append = 0;
863 + struct option opts[] = {
864 + CONFIG_LOCATION_OPTIONS,
865 + CONFIG_TYPE_OPTIONS,
866 + OPT_GROUP(N_("Filter")),
867 + OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
868 + OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
869 + OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
870 + OPT_GROUP(N_("Other")),
871 + OPT_STRING(0, "comment", &comment_arg, N_("value"), N_("human-readable comment string (# will be prepended as needed)")),
872 + OPT_BOOL(0, "append", &append, N_("add a new line without altering any existing values")),
873 + OPT_END(),
874 + };
875 + struct key_value_info default_kvi = KVI_INIT;
876 + char *value;
877 + int ret;
878 +
879 + argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
880 + PARSE_OPT_STOP_AT_NON_OPTION);
881 + check_write();
882 + check_argc(argc, 2, 2);
883 +
884 + if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
885 + die(_("--fixed-value only applies with --value=<pattern>"));
886 + if (append && value_pattern)
887 + die(_("--append cannot be used with --value=<pattern>"));
888 + if (append)
889 + value_pattern = CONFIG_REGEX_NONE;
890 +
891 + comment = git_config_prepare_comment_string(comment_arg);
892 +
893 + handle_config_location(prefix);
894 +
895 + value = normalize_value(argv[0], argv[1], &default_kvi);
896 +
897 + if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {
898 + ret = git_config_set_multivar_in_file_gently(given_config_source.file,
899 + argv[0], value, value_pattern,
900 + comment, flags);
901 + } else {
902 + ret = git_config_set_in_file_gently(given_config_source.file,
903 + argv[0], comment, value);
904 + if (ret == CONFIG_NOTHING_SET)
905 + error(_("cannot overwrite multiple values with a single value\n"
906 + " Use a regexp, --add or --replace-all to change %s."), argv[0]);
907 + }
908 +
909 + free(comment);
910 + free(value);
911 + return ret;
912 +}
913 +
914 static struct option builtin_subcommand_options[] = {
915 OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
916 OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
917 + OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
918 OPT_END(),
919 };
920
t/t1300-config.sh
+53 -49
@@ -20,12 +20,16 @@ legacy)
20 mode_get=""
21 mode_get_all="--get-all"
22 mode_get_regexp="--get-regexp"
23 + mode_set=""
24 + mode_replace_all="--replace-all"
25 ;;
26 subcommands)
27 mode_prefix=""
28 mode_get="get"
29 mode_get_all="get --all"
30 mode_get_regexp="get --regexp --all --show-names"
31 + mode_set="set"
32 + mode_replace_all="set --all"
33 ;;
34 *)
35 BUG "unknown mode $mode";;
@@ -132,7 +136,7 @@ cat > expect << EOF
136 penguin = little blue
137 EOF
138 test_expect_success 'initial' '
135 - git config section.penguin "little blue" &&
139 + git config ${mode_set} section.penguin "little blue" &&
140 test_cmp expect .git/config
141 '
142
@@ -142,7 +146,7 @@ cat > expect << EOF
146 Movie = BadPhysics
147 EOF
148 test_expect_success 'mixed case' '
145 - git config Section.Movie BadPhysics &&
149 + git config ${mode_set} Section.Movie BadPhysics &&
150 test_cmp expect .git/config
151 '
152
@@ -154,7 +158,7 @@ cat > expect << EOF
158 WhatEver = Second
159 EOF
160 test_expect_success 'similar section' '
157 - git config Sections.WhatEver Second &&
161 + git config ${mode_set} Sections.WhatEver Second &&
162 test_cmp expect .git/config
163 '
164
@@ -167,7 +171,7 @@ cat > expect << EOF
171 WhatEver = Second
172 EOF
173 test_expect_success 'uppercase section' '
170 - git config SECTION.UPPERCASE true &&
174 + git config ${mode_set} SECTION.UPPERCASE true &&
175 test_cmp expect .git/config
176 '
177
@@ -194,8 +198,8 @@ EOF
198
199 test_expect_success 'append comments' '
200 git config --replace-all --comment="Pygoscelis papua" section.penguin gentoo &&
197 - git config --comment="find fish" section.disposition peckish &&
198 - git config --comment="#abc" section.foo bar &&
201 + git config ${mode_set} --comment="find fish" section.disposition peckish &&
202 + git config ${mode_set} --comment="#abc" section.foo bar &&
203
204 git config --comment="and comment" section.spsp value &&
205 git config --comment=" # and comment" section.htsp value &&
@@ -204,7 +208,7 @@ test_expect_success 'append comments' '
208 '
209
210 test_expect_success 'Prohibited LF in comment' '
207 - test_must_fail git config --comment="a${LF}b" section.k v
211 + test_must_fail git config ${mode_set} --comment="a${LF}b" section.k v
212 '
213
214 test_expect_success 'non-match result' 'test_cmp expect .git/config'
@@ -301,14 +305,14 @@ test_expect_success 'multiple unset is correct' '
305 cp .git/config2 .git/config
306
307 test_expect_success '--replace-all missing value' '
304 - test_must_fail git config --replace-all beta.haha &&
308 + test_must_fail git config ${mode_replace_all} beta.haha &&
309 test_cmp .git/config2 .git/config
310 '
311
312 rm .git/config2
313
314 test_expect_success '--replace-all' '
311 - git config --replace-all beta.haha gamma
315 + git config ${mode_replace_all} beta.haha gamma
316 '
317
318 cat > expect << EOF
@@ -335,7 +339,7 @@ noIndent= sillyValue ; 'nother silly comment
339 [nextSection] noNewline = ouch
340 EOF
341 test_expect_success 'really mean test' '
338 - git config beta.haha alpha &&
342 + git config ${mode_set} beta.haha alpha &&
343 test_cmp expect .git/config
344 '
345
@@ -350,7 +354,7 @@ noIndent= sillyValue ; 'nother silly comment
354 nonewline = wow
355 EOF
356 test_expect_success 'really really mean test' '
353 - git config nextsection.nonewline wow &&
357 + git config ${mode_set} nextsection.nonewline wow &&
358 test_cmp expect .git/config
359 '
360
@@ -824,16 +828,16 @@ EOF
828
829 test_expect_success 'section ending' '
830 rm -f .git/config &&
827 - git config gitcvs.enabled true &&
828 - git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
829 - git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
831 + git config ${mode_set} gitcvs.enabled true &&
832 + git config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
833 + git config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
834 test_cmp expect .git/config
835
836 '
837
838 test_expect_success numbers '
835 - git config kilo.gram 1k &&
836 - git config mega.ton 1m &&
839 + git config ${mode_set} kilo.gram 1k &&
840 + git config ${mode_set} mega.ton 1m &&
841 echo 1024 >expect &&
842 echo 1048576 >>expect &&
843 git config --int --get kilo.gram >actual &&
@@ -842,20 +846,20 @@ test_expect_success numbers '
846 '
847
848 test_expect_success '--int is at least 64 bits' '
845 - git config giga.watts 121g &&
849 + git config ${mode_set} giga.watts 121g &&
850 echo >expect &&
851 test_cmp_config 129922760704 --int --get giga.watts
852 '
853
854 test_expect_success 'invalid unit' '
851 - git config aninvalid.unit "1auto" &&
855 + git config ${mode_set} aninvalid.unit "1auto" &&
856 test_cmp_config 1auto aninvalid.unit &&
857 test_must_fail git config --int --get aninvalid.unit 2>actual &&
858 test_grep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual
859 '
860
861 test_expect_success 'invalid unit boolean' '
858 - git config commit.gpgsign "1true" &&
862 + git config ${mode_set} commit.gpgsign "1true" &&
863 test_cmp_config 1true commit.gpgsign &&
864 test_must_fail git config --bool --get commit.gpgsign 2>actual &&
865 test_grep "bad boolean config value .1true. for .commit.gpgsign." actual
@@ -885,14 +889,14 @@ EOF
889
890 test_expect_success bool '
891
888 - git config bool.true1 01 &&
889 - git config bool.true2 -1 &&
890 - git config bool.true3 YeS &&
891 - git config bool.true4 true &&
892 - git config bool.false1 000 &&
893 - git config bool.false2 "" &&
894 - git config bool.false3 nO &&
895 - git config bool.false4 FALSE &&
892 + git config ${mode_set} bool.true1 01 &&
893 + git config ${mode_set} bool.true2 -1 &&
894 + git config ${mode_set} bool.true3 YeS &&
895 + git config ${mode_set} bool.true4 true &&
896 + git config ${mode_set} bool.false1 000 &&
897 + git config ${mode_set} bool.false2 "" &&
898 + git config ${mode_set} bool.false3 nO &&
899 + git config ${mode_set} bool.false4 FALSE &&
900 rm -f result &&
901 for i in 1 2 3 4
902 do
@@ -903,7 +907,7 @@ test_expect_success bool '
907
908 test_expect_success 'invalid bool (--get)' '
909
906 - git config bool.nobool foobar &&
910 + git config ${mode_set} bool.nobool foobar &&
911 test_must_fail git config --bool --get bool.nobool'
912
913 test_expect_success 'invalid bool (set)' '
@@ -1092,7 +1096,7 @@ test_expect_success 'get --expiry-date' '
1096
1097 test_expect_success 'get --type=color' '
1098 rm .git/config &&
1095 - git config foo.color "red" &&
1099 + git config ${mode_set} foo.color "red" &&
1100 git config --get --type=color foo.color >actual.raw &&
1101 test_decode_color <actual.raw >actual &&
1102 echo "<RED>" >expect &&
@@ -1129,10 +1133,10 @@ cat > expect << EOF
1133 EOF
1134 test_expect_success 'quoting' '
1135 rm -f .git/config &&
1132 - git config quote.leading " test" &&
1133 - git config quote.ending "test " &&
1134 - git config quote.semicolon "test;test" &&
1135 - git config quote.hash "test#test" &&
1136 + git config ${mode_set} quote.leading " test" &&
1137 + git config ${mode_set} quote.ending "test " &&
1138 + git config ${mode_set} quote.semicolon "test;test" &&
1139 + git config ${mode_set} quote.hash "test#test" &&
1140 test_cmp expect .git/config
1141 '
1142
@@ -1140,7 +1144,7 @@ test_expect_success 'key with newline' '
1144 test_must_fail git config ${mode_get} "key.with
1145 newline" 123'
1146
1143 -test_expect_success 'value with newline' 'git config key.sub value.with\\\
1147 +test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\
1148 newline'
1149
1150 cat > .git/config <<\EOF
@@ -1199,21 +1203,21 @@ test_expect_success '--null --get-regexp' '
1203
1204 test_expect_success 'inner whitespace kept verbatim, spaces only' '
1205 echo "foo bar" >expect &&
1202 - git config section.val "foo bar" &&
1206 + git config ${mode_set} section.val "foo bar" &&
1207 git config ${mode_get} section.val >actual &&
1208 test_cmp expect actual
1209 '
1210
1211 test_expect_success 'inner whitespace kept verbatim, horizontal tabs only' '
1212 echo "fooQQbar" | q_to_tab >expect &&
1209 - git config section.val "$(cat expect)" &&
1213 + git config ${mode_set} section.val "$(cat expect)" &&
1214 git config ${mode_get} section.val >actual &&
1215 test_cmp expect actual
1216 '
1217
1218 test_expect_success 'inner whitespace kept verbatim, horizontal tabs and spaces' '
1219 echo "foo Q bar" | q_to_tab >expect &&
1216 - git config section.val "$(cat expect)" &&
1220 + git config ${mode_set} section.val "$(cat expect)" &&
1221 git config ${mode_get} section.val >actual &&
1222 test_cmp expect actual
1223 '
@@ -1252,12 +1256,12 @@ test_expect_success 'check split_cmdline return' '
1256 git init repo &&
1257 (
1258 cd repo &&
1255 - git config alias.split-cmdline-fix "echo \"" &&
1259 + git config ${mode_set} alias.split-cmdline-fix "echo \"" &&
1260 test_must_fail git split-cmdline-fix &&
1261 echo foo >foo &&
1262 git add foo &&
1263 git commit -m "initial commit" &&
1260 - git config branch.main.mergeoptions "echo \"" &&
1264 + git config ${mode_set} branch.main.mergeoptions "echo \"" &&
1265 test_must_fail git merge main
1266 )
1267 '
@@ -1295,12 +1299,12 @@ test_expect_success 'key sanity-checking' '
1299 test_must_fail git config ${mode_get} foo.1bar &&
1300 test_must_fail git config ${mode_get} foo."ba
1301 z".bar &&
1298 - test_must_fail git config . false &&
1299 - test_must_fail git config .foo false &&
1300 - test_must_fail git config foo. false &&
1301 - test_must_fail git config .foo. false &&
1302 - git config foo.bar true &&
1303 - git config foo."ba =z".bar false
1302 + test_must_fail git config ${mode_set} . false &&
1303 + test_must_fail git config ${mode_set} .foo false &&
1304 + test_must_fail git config ${mode_set} foo. false &&
1305 + test_must_fail git config ${mode_set} .foo. false &&
1306 + git config ${mode_set} foo.bar true &&
1307 + git config ${mode_set} foo."ba =z".bar false
1308 '
1309
1310 test_expect_success 'git -c works with aliases of builtins' '
@@ -2523,7 +2527,7 @@ test_expect_success '--replace-all does not invent newlines' '
2527 [abc]
2528 Qkey = b
2529 EOF
2526 - git config --replace-all abc.key b &&
2530 + git config ${mode_replace_all} abc.key b &&
2531 test_cmp expect .git/config
2532 '
2533
@@ -2595,8 +2599,8 @@ test_expect_success 'refuse --fixed-value for incompatible actions' '
2599 test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
2600
2601 # These modes complain when --fixed-value has no value-pattern
2598 - test_must_fail git config --file=config --fixed-value dev.null bogus &&
2599 - test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
2602 + test_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&
2603 + test_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&
2604 test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
2605 test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
2606 test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
@@ -2637,7 +2641,7 @@ test_expect_success '--fixed-value uses exact string matching' '
2641 test_must_fail git config ${mode_get} --file=config fixed.test &&
2642
2643 cp initial config &&
2640 - git config --file=config --replace-all fixed.test bogus "$META" &&
2644 + git config --file=config fixed.test bogus "$META" &&
2645 git config ${mode_prefix}list --file=config >actual &&
2646 cat >expect <<-EOF &&
2647 fixed.test=$META