diff mbox series

[v3,08/13] builtin/config: introduce "set" subcommand

Message ID e2815affab8c1dce9e2112920ec94a237bd476e0.1711527812.git.ps@pks.im (mailing list archive)
State New
Headers show
Series builtin/config: introduce subcommands | expand

Commit Message

Patrick Steinhardt March 27, 2024, 8:46 a.m. UTC
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>
---
 Documentation/git-config.txt | 38 +++++++++------
 builtin/config.c             | 57 ++++++++++++++++++++++
 t/t1300-config.sh            | 92 +++++++++++++++++++-----------------
 3 files changed, 129 insertions(+), 58 deletions(-)
diff mbox series

Patch

diff --git a/Documentation/git-config.txt b/Documentation/git-config.txt
index 4a7d6b7543..7fdf9b434e 100644
--- a/Documentation/git-config.txt
+++ b/Documentation/git-config.txt
@@ -11,9 +11,7 @@  SYNOPSIS
 [verse]
 'git config list' [<file-option>] [<display-option>] [--includes]
 'git config get' [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>
-'git config' [<file-option>] [--type=<type>] [--fixed-value] [--show-origin] [--show-scope] [-z|--null] <name> [<value> [<value-pattern>]]
-'git config' [<file-option>] [--type=<type>] --add <name> <value>
-'git config' [<file-option>] [--type=<type>] [--fixed-value] --replace-all <name> <value> [<value-pattern>]
+'git config set' [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>
 'git config' [<file-option>] [--fixed-value] --unset <name> [<value-pattern>]
 'git config' [<file-option>] [--fixed-value] --unset-all <name> [<value-pattern>]
 '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
 actually the section and the key separated by a dot, and the value will be
 escaped.
 
-Multiple lines can be added to an option by using the `--add` option.
+Multiple lines can be added to an option by using the `--append` option.
 If you want to update or unset an option which can occur on multiple
 lines, a `value-pattern` (which is an extended regular expression,
 unless the `--fixed-value` option is given) needs to be given.  Only the
@@ -82,6 +80,13 @@  get::
 	emits all values associated with key. Returns error code 1 if key is
 	not present.
 
+set::
+	Set value for one or more config options. By default, this command
+	refuses to write multi-valued config options. Passing `--all` will
+	replace all multi-valued config options with the new value, whereas
+	`--value=` will replace all config options whose values match the given
+	pattern.
+
 [[OPTIONS]]
 OPTIONS
 -------
@@ -90,10 +95,9 @@  OPTIONS
 	Default behavior is to replace at most one line. This replaces
 	all lines matching the key (and optionally the `value-pattern`).
 
---add::
+--append::
 	Adds a new line to the option without altering any existing
-	values.  This is the same as providing '^$' as the `value-pattern`
-	in `--replace-all`.
+	values. This is the same as providing '--value=^$' in `set`.
 
 --all::
 	With `get`, Return all values for a multi-valued key.
@@ -284,6 +288,9 @@  recommended to migrate to the new syntax.
 'git config <name>'::
 	Replaced by `git config get <name>`.
 
+'git config <name> <value> [<value-pattern>]'::
+	Replaced by `git config set [--value=<pattern>] <name> <value>`.
+
 -l::
 --list::
 	Replaced by `git config list`.
@@ -303,6 +310,9 @@  recommended to migrate to the new syntax.
 --get-color <name> [<default>]::
 	Replaced by `git config get --type=color [--default=<default>] <name>`.
 
+--add <name> <value>::
+	Replaced by `git config set --append <name> <value>`.
+
 CONFIGURATION
 -------------
 `pager.config` is only respected when listing configuration, i.e., when
@@ -349,7 +359,7 @@  precedence over values read earlier.  When multiple values are taken then all
 values of a key from all files will be used.
 
 By default, options are only written to the repository specific
-configuration file. Note that this also affects options like `--replace-all`
+configuration file. Note that this also affects options like `set`
 and `--unset`. *'git config' will only ever change one file at a time*.
 
 You can limit which configuration sources are read from or written to by
@@ -485,7 +495,7 @@  Given a .git/config like this:
 you can set the filemode to true with
 
 ------------
-% git config core.filemode true
+% git config set core.filemode true
 ------------
 
 The hypothetical proxy command entries actually have a postfix to discern
@@ -493,7 +503,7 @@  what URL they apply to. Here is how to change the entry for kernel.org
 to "ssh".
 
 ------------
-% git config core.gitproxy '"ssh" for kernel.org' 'for kernel.org$'
+% git config set --value='for kernel.org$' core.gitproxy '"ssh" for kernel.org'
 ------------
 
 This makes sure that only the key/value pair for kernel.org is replaced.
@@ -529,26 +539,26 @@  If you like to live dangerously, you can replace *all* core.gitproxy by a
 new one with
 
 ------------
-% git config --replace-all core.gitproxy ssh
+% git config set --all core.gitproxy ssh
 ------------
 
 However, if you really only want to replace the line for the default proxy,
 i.e. the one without a "for ..." postfix, do something like this:
 
 ------------
-% git config core.gitproxy ssh '! for '
+% git config set --value='! for ' core.gitproxy ssh
 ------------
 
 To actually match only values with an exclamation mark, you have to
 
 ------------
-% git config section.key value '[!]'
+% git config set --value='[!]' section.key value
 ------------
 
 To add a new proxy, without altering any of the existing ones, use
 
 ------------
-% git config --add core.gitproxy '"proxy-command" for example.com'
+% git config set --append core.gitproxy '"proxy-command" for example.com'
 ------------
 
 An example to use customized color from the configuration in your
diff --git a/builtin/config.c b/builtin/config.c
index 447500dbd2..eec57fbd17 100644
--- a/builtin/config.c
+++ b/builtin/config.c
@@ -18,6 +18,7 @@ 
 static const char *const builtin_config_usage[] = {
 	N_("git config list [<file-option>] [<display-option>] [--includes]"),
 	N_("git config get [<file-option>] [<display-option>] [--includes] [--all] [--regexp=<regexp>] [--value=<value>] [--fixed-value] [--default=<default>] <name>"),
+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
 	NULL
 };
 
@@ -31,6 +32,11 @@  static const char *const builtin_config_get_usage[] = {
 	NULL
 };
 
+static const char *const builtin_config_set_usage[] = {
+	N_("git config set [<file-option>] [--type=<type>] [--all] [--value=<value>] [--fixed-value] <name> <value>"),
+	NULL
+};
+
 static char *key;
 static regex_t *key_regexp;
 static const char *value_pattern;
@@ -847,9 +853,60 @@  static int cmd_config_get(int argc, const char **argv, const char *prefix)
 	return get_value(argv[0], value_pattern, flags);
 }
 
+static int cmd_config_set(int argc, const char **argv, const char *prefix)
+{
+	const char *value_pattern = NULL;
+	int flags = 0, append = 0;
+	struct option opts[] = {
+		CONFIG_LOCATION_OPTIONS,
+		CONFIG_TYPE_OPTIONS,
+		OPT_GROUP(N_("Filter")),
+		OPT_BIT(0, "all", &flags, N_("replace multi-valued config option with new value"), CONFIG_FLAGS_MULTI_REPLACE),
+		OPT_STRING(0, "value", &value_pattern, N_("pattern"), N_("show config with values matching the pattern")),
+		OPT_BIT(0, "fixed-value", &flags, N_("use string equality when comparing values to value pattern"), CONFIG_FLAGS_FIXED_VALUE),
+		OPT_GROUP(N_("Other")),
+		OPT_BOOL(0, "append", &append, N_("add a new line without altering any existing values")),
+		OPT_END(),
+	};
+	struct key_value_info default_kvi = KVI_INIT;
+	char *value;
+	int ret;
+
+	argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	check_write();
+	check_argc(argc, 2, 2);
+
+	if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)
+		die(_("--fixed-value only applies with --value=<pattern>"));
+	if (append && value_pattern)
+		die(_("--append cannot be used with --value=<pattern>"));
+	if (append)
+		value_pattern = CONFIG_REGEX_NONE;
+
+	handle_config_location(prefix);
+
+	value = normalize_value(argv[0], argv[1], &default_kvi);
+
+	if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) {
+		ret = git_config_set_multivar_in_file_gently(given_config_source.file,
+							     argv[0], value, value_pattern,
+							     flags);
+	} else {
+		ret = git_config_set_in_file_gently(given_config_source.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]);
+	}
+
+	free(value);
+	return ret;
+}
+
 static struct option builtin_subcommand_options[] = {
 	OPT_SUBCOMMAND("list", &subcommand, cmd_config_list),
 	OPT_SUBCOMMAND("get", &subcommand, cmd_config_get),
+	OPT_SUBCOMMAND("set", &subcommand, cmd_config_set),
 	OPT_END(),
 };
 
diff --git a/t/t1300-config.sh b/t/t1300-config.sh
index f1547921c6..f435b8f194 100755
--- a/t/t1300-config.sh
+++ b/t/t1300-config.sh
@@ -20,12 +20,16 @@  legacy)
 	mode_get=""
 	mode_get_all="--get-all"
 	mode_get_regexp="--get-regexp"
+	mode_set=""
+	mode_replace_all="--replace-all"
 	;;
 subcommands)
 	mode_prefix=""
 	mode_get="get"
 	mode_get_all="get --all"
 	mode_get_regexp="get --regexp --all --show-names"
+	mode_set="set"
+	mode_replace_all="set --all"
 	;;
 *)
 	BUG "unknown mode $mode";;
@@ -40,7 +44,7 @@  cat > expect << EOF
 	penguin = little blue
 EOF
 test_expect_success 'initial' '
-	git config section.penguin "little blue" &&
+	git config ${mode_set} section.penguin "little blue" &&
 	test_cmp expect .git/config
 '
 
@@ -50,7 +54,7 @@  cat > expect << EOF
 	Movie = BadPhysics
 EOF
 test_expect_success 'mixed case' '
-	git config Section.Movie BadPhysics &&
+	git config ${mode_set} Section.Movie BadPhysics &&
 	test_cmp expect .git/config
 '
 
@@ -62,7 +66,7 @@  cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'similar section' '
-	git config Sections.WhatEver Second &&
+	git config ${mode_set} Sections.WhatEver Second &&
 	test_cmp expect .git/config
 '
 
@@ -75,7 +79,7 @@  cat > expect << EOF
 	WhatEver = Second
 EOF
 test_expect_success 'uppercase section' '
-	git config SECTION.UPPERCASE true &&
+	git config ${mode_set} SECTION.UPPERCASE true &&
 	test_cmp expect .git/config
 '
 
@@ -191,14 +195,14 @@  test_expect_success 'multiple unset is correct' '
 cp .git/config2 .git/config
 
 test_expect_success '--replace-all missing value' '
-	test_must_fail git config --replace-all beta.haha &&
+	test_must_fail git config ${mode_replace_all} beta.haha &&
 	test_cmp .git/config2 .git/config
 '
 
 rm .git/config2
 
 test_expect_success '--replace-all' '
-	git config --replace-all beta.haha gamma
+	git config ${mode_replace_all} beta.haha gamma
 '
 
 cat > expect << EOF
@@ -225,7 +229,7 @@  noIndent= sillyValue ; 'nother silly comment
 [nextSection] noNewline = ouch
 EOF
 test_expect_success 'really mean test' '
-	git config beta.haha alpha &&
+	git config ${mode_set} beta.haha alpha &&
 	test_cmp expect .git/config
 '
 
@@ -240,7 +244,7 @@  noIndent= sillyValue ; 'nother silly comment
 	nonewline = wow
 EOF
 test_expect_success 'really really mean test' '
-	git config nextsection.nonewline wow &&
+	git config ${mode_set} nextsection.nonewline wow &&
 	test_cmp expect .git/config
 '
 
@@ -714,16 +718,16 @@  EOF
 
 test_expect_success 'section ending' '
 	rm -f .git/config &&
-	git config gitcvs.enabled true &&
-	git config gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
-	git config gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.enabled true &&
+	git config ${mode_set} gitcvs.ext.dbname %Ggitcvs1.%a.%m.sqlite &&
+	git config ${mode_set} gitcvs.dbname %Ggitcvs2.%a.%m.sqlite &&
 	test_cmp expect .git/config
 
 '
 
 test_expect_success numbers '
-	git config kilo.gram 1k &&
-	git config mega.ton 1m &&
+	git config ${mode_set} kilo.gram 1k &&
+	git config ${mode_set} mega.ton 1m &&
 	echo 1024 >expect &&
 	echo 1048576 >>expect &&
 	git config --int --get kilo.gram >actual &&
@@ -732,20 +736,20 @@  test_expect_success numbers '
 '
 
 test_expect_success '--int is at least 64 bits' '
-	git config giga.watts 121g &&
+	git config ${mode_set} giga.watts 121g &&
 	echo  >expect &&
 	test_cmp_config 129922760704 --int --get giga.watts
 '
 
 test_expect_success 'invalid unit' '
-	git config aninvalid.unit "1auto" &&
+	git config ${mode_set} aninvalid.unit "1auto" &&
 	test_cmp_config 1auto aninvalid.unit &&
 	test_must_fail git config --int --get aninvalid.unit 2>actual &&
 	test_grep "bad numeric config value .1auto. for .aninvalid.unit. in file .git/config: invalid unit" actual
 '
 
 test_expect_success 'invalid unit boolean' '
-	git config commit.gpgsign "1true" &&
+	git config ${mode_set} commit.gpgsign "1true" &&
 	test_cmp_config 1true commit.gpgsign &&
 	test_must_fail git config --bool --get commit.gpgsign 2>actual &&
 	test_grep "bad boolean config value .1true. for .commit.gpgsign." actual
@@ -775,14 +779,14 @@  EOF
 
 test_expect_success bool '
 
-	git config bool.true1 01 &&
-	git config bool.true2 -1 &&
-	git config bool.true3 YeS &&
-	git config bool.true4 true &&
-	git config bool.false1 000 &&
-	git config bool.false2 "" &&
-	git config bool.false3 nO &&
-	git config bool.false4 FALSE &&
+	git config ${mode_set} bool.true1 01 &&
+	git config ${mode_set} bool.true2 -1 &&
+	git config ${mode_set} bool.true3 YeS &&
+	git config ${mode_set} bool.true4 true &&
+	git config ${mode_set} bool.false1 000 &&
+	git config ${mode_set} bool.false2 "" &&
+	git config ${mode_set} bool.false3 nO &&
+	git config ${mode_set} bool.false4 FALSE &&
 	rm -f result &&
 	for i in 1 2 3 4
 	do
@@ -793,7 +797,7 @@  test_expect_success bool '
 
 test_expect_success 'invalid bool (--get)' '
 
-	git config bool.nobool foobar &&
+	git config ${mode_set} bool.nobool foobar &&
 	test_must_fail git config --bool --get bool.nobool'
 
 test_expect_success 'invalid bool (set)' '
@@ -982,7 +986,7 @@  test_expect_success 'get --expiry-date' '
 
 test_expect_success 'get --type=color' '
 	rm .git/config &&
-	git config foo.color "red" &&
+	git config ${mode_set} foo.color "red" &&
 	git config --get --type=color foo.color >actual.raw &&
 	test_decode_color <actual.raw >actual &&
 	echo "<RED>" >expect &&
@@ -1019,10 +1023,10 @@  cat > expect << EOF
 EOF
 test_expect_success 'quoting' '
 	rm -f .git/config &&
-	git config quote.leading " test" &&
-	git config quote.ending "test " &&
-	git config quote.semicolon "test;test" &&
-	git config quote.hash "test#test" &&
+	git config ${mode_set} quote.leading " test" &&
+	git config ${mode_set} quote.ending "test " &&
+	git config ${mode_set} quote.semicolon "test;test" &&
+	git config ${mode_set} quote.hash "test#test" &&
 	test_cmp expect .git/config
 '
 
@@ -1030,7 +1034,7 @@  test_expect_success 'key with newline' '
 	test_must_fail git config ${mode_get} "key.with
 newline" 123'
 
-test_expect_success 'value with newline' 'git config key.sub value.with\\\
+test_expect_success 'value with newline' 'git config ${mode_set} key.sub value.with\\\
 newline'
 
 cat > .git/config <<\EOF
@@ -1088,7 +1092,7 @@  test_expect_success '--null --get-regexp' '
 '
 
 test_expect_success 'inner whitespace kept verbatim' '
-	git config section.val "foo 	  bar" &&
+	git config ${mode_set} section.val "foo 	  bar" &&
 	test_cmp_config "foo 	  bar" section.val
 '
 
@@ -1126,12 +1130,12 @@  test_expect_success 'check split_cmdline return' '
 	git init repo &&
 	(
 		cd repo &&
-		git config alias.split-cmdline-fix "echo \"" &&
+		git config ${mode_set} alias.split-cmdline-fix "echo \"" &&
 		test_must_fail git split-cmdline-fix &&
 		echo foo >foo &&
 		git add foo &&
 		git commit -m "initial commit" &&
-		git config branch.main.mergeoptions "echo \"" &&
+		git config ${mode_set} branch.main.mergeoptions "echo \"" &&
 		test_must_fail git merge main
 	)
 '
@@ -1169,12 +1173,12 @@  test_expect_success 'key sanity-checking' '
 	test_must_fail git config ${mode_get} foo.1bar &&
 	test_must_fail git config ${mode_get} foo."ba
 				z".bar &&
-	test_must_fail git config . false &&
-	test_must_fail git config .foo false &&
-	test_must_fail git config foo. false &&
-	test_must_fail git config .foo. false &&
-	git config foo.bar true &&
-	git config foo."ba =z".bar false
+	test_must_fail git config ${mode_set} . false &&
+	test_must_fail git config ${mode_set} .foo false &&
+	test_must_fail git config ${mode_set} foo. false &&
+	test_must_fail git config ${mode_set} .foo. false &&
+	git config ${mode_set} foo.bar true &&
+	git config ${mode_set} foo."ba =z".bar false
 '
 
 test_expect_success 'git -c works with aliases of builtins' '
@@ -2397,7 +2401,7 @@  test_expect_success '--replace-all does not invent newlines' '
 	[abc]
 	Qkey = b
 	EOF
-	git config --replace-all abc.key b &&
+	git config ${mode_replace_all} abc.key b &&
 	test_cmp expect .git/config
 '
 
@@ -2469,8 +2473,8 @@  test_expect_success 'refuse --fixed-value for incompatible actions' '
 	test_must_fail git config --file=config --fixed-value --get-colorbool dev.null &&
 
 	# These modes complain when --fixed-value has no value-pattern
-	test_must_fail git config --file=config --fixed-value dev.null bogus &&
-	test_must_fail git config --file=config --fixed-value --replace-all dev.null bogus &&
+	test_must_fail git config ${mode_set} --file=config --fixed-value dev.null bogus &&
+	test_must_fail git config ${mode_replace_all} --file=config --fixed-value dev.null bogus &&
 	test_must_fail git config ${mode_prefix}get --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_all} --file=config --fixed-value dev.null &&
 	test_must_fail git config ${mode_get_regexp} --file=config --fixed-value "dev.*" &&
@@ -2511,7 +2515,7 @@  test_expect_success '--fixed-value uses exact string matching' '
 	test_must_fail git config ${mode_get} --file=config fixed.test &&
 
 	cp initial config &&
-	git config --file=config --replace-all fixed.test bogus "$META" &&
+	git config --file=config fixed.test bogus "$META" &&
 	git config ${mode_prefix}list --file=config >actual &&
 	cat >expect <<-EOF &&
 	fixed.test=$META