Subject: [PATCH 09/11] config-batch: add 'set' v1 command

From: "Derrick Stolee via GitGitGadget" <[email-masked]>
Date: Wed, 04 Feb 2026 14:20:01 +0000

From: Derrick Stolee <[email-masked]>

This new command is intended for single-value assignments to a specific
chosen scope. More complicated versions of the 'git config set' command
will be incorporated into future commands.

Signed-off-by: Derrick Stolee <[email-masked]>
---
 Documentation/git-config-batch.adoc | 24 ++++++++
 builtin/config-batch.c              | 71 ++++++++++++++++++++++
 config.c                            | 27 +++++++++
 config.h                            |  3 +
 t/t1312-config-batch.sh             | 94 ++++++++++++++++++++++++++++-
 5 files changed, 217 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc
index 3c9a3bb763..feec85c4ef 100644
--- a/Documentation/git-config-batch.adoc
+++ b/Documentation/git-config-batch.adoc
@@ -111,6 +111,30 @@ get 1 missing <key> [<value-pattern>|<value>]
 where `<value-pattern>` or `<value>` is only supplied if provided in
 the command.
 
+`set` version 1::
+	The `set` command writes a single key-value pair to a config
+	file. It specifies which file by a `<scope>` parameter from
+	among `system`, `global`, `local`, and `worktree`. The `<key>`
+	is the next positional argument. The remaining data in the line
+	is provided as the `<value>` to assign the config.
++
+------------
+set 1 <scope> <key> <value>
+------------
++
+These uses will match the behavior of `git config --set --<scope> <key>
+<value>`. Note that replacing all values with the `--all` option or
+matching specific value patterns are not supported by this command.
++
+The response of these commands will include a `success` message if the
+value is written as expected or `failed` if an unexpected failure
+occurs:
++
+------------
+set 1 success <scope> <key> <value>
+set 1 failed <scope> <key> <value>
+------------
+
 NUL-Terminated Format
 ~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/builtin/config-batch.c b/builtin/config-batch.c
index 9829b16c6f..373b0cad47 100644
--- a/builtin/config-batch.c
+++ b/builtin/config-batch.c
@@ -16,6 +16,7 @@ static int zformat = 0;
 #define UNKNOWN_COMMAND "unknown_command"
 #define HELP_COMMAND "help"
 #define GET_COMMAND "get"
+#define SET_COMMAND "set"
 #define COMMAND_PARSE_ERROR "command_parse_error"
 
 static void print_word(const char *word, int start)
@@ -379,6 +380,71 @@ cleanup:
 	return res;
 }
 
+
+/**
+ * 'set' command, version 1.
+ *
+ * Positional arguments should be of the form:
+ *
+ * [0] scope ("system", "global", "local", or "worktree")
+ * [1] config key
+ * [2] config value
+ */
+static int set_command_1(struct repository *repo,
+			 const char *prefix,
+			 char *data,
+			 size_t data_len)
+{
+	int res = 0, err = 0;
+	enum config_scope scope = CONFIG_SCOPE_UNKNOWN;
+	char *token = NULL, *key = NULL, *value = NULL;
+	struct config_location_options locopts = CONFIG_LOCATION_OPTIONS_INIT;
+
+	if (!parse_token(&data, &data_len, &token, &err) || err)
+		goto parse_error;
+
+	if (parse_scope(token, &scope) ||
+	    scope == CONFIG_SCOPE_UNKNOWN ||
+	    scope == CONFIG_SCOPE_SUBMODULE ||
+	    scope == CONFIG_SCOPE_COMMAND)
+		goto parse_error;
+
+	if (!parse_token(&data, &data_len, &key, &err) || err)
+		goto parse_error;
+
+	/* Use the remaining data as the value string. */
+	if (!zformat)
+		value = data;
+	else {
+		parse_token(&data, &data_len, &value, &err);
+		if (err)
+			goto parse_error;
+	}
+
+	if (location_options_set_scope(&locopts, scope))
+		goto parse_error;
+	location_options_init(repo, &locopts, prefix);
+
+	res = repo_config_set_in_file_gently(repo, locopts.source.file,
+					     key, NULL, value);
+
+	if (res)
+		res = emit_response(SET_COMMAND, "1", "failure",
+				    scope_str(scope), key, value, NULL);
+	else
+		res = emit_response(SET_COMMAND, "1", "success",
+				    scope_str(scope), key, value, NULL);
+
+	goto cleanup;
+
+parse_error:
+	res = command_parse_error(SET_COMMAND);
+
+cleanup:
+	location_options_release(&locopts);
+	return res;
+}
+
 struct command {
 	const char *name;
 	command_fn fn;
@@ -396,6 +462,11 @@ static struct command commands[] = {
 		.fn = get_command_1,
 		.version = 1,
 	},
+	{
+		.name = SET_COMMAND,
+		.fn = set_command_1,
+		.version = 1,
+	},
 	/* unknown_command must be last. */
 	{
 		.name = "",
diff --git a/config.c b/config.c
index 9f1a7b45cf..fa72234750 100644
--- a/config.c
+++ b/config.c
@@ -3594,6 +3594,33 @@ int lookup_config(const char **mapping, int nr_mapping, const char *var)
 	return -1;
 }
 
+int location_options_set_scope(struct config_location_options *opts,
+			       enum config_scope scope)
+{
+	switch (scope) {
+	case CONFIG_SCOPE_SYSTEM:
+		opts->use_system_config = 1;
+		break;
+
+	case CONFIG_SCOPE_GLOBAL:
+		opts->use_global_config = 1;
+		break;
+
+	case CONFIG_SCOPE_LOCAL:
+		opts->use_local_config = 1;
+		break;
+
+	case CONFIG_SCOPE_WORKTREE:
+		opts->use_worktree_config = 1;
+		break;
+
+	default:
+		return -1;
+	}
+
+	return 0;
+}
+
 void location_options_init(struct repository *repo,
 			   struct config_location_options *opts,
 			   const char *prefix)
diff --git a/config.h b/config.h
index 6663964977..f6432c1ec2 100644
--- a/config.h
+++ b/config.h
@@ -180,6 +180,9 @@ struct config_location_options {
 	.respect_includes_opt = -1, \
 }
 
+int location_options_set_scope(struct config_location_options *opts,
+			       enum config_scope scope);
+
 void location_options_init(struct repository *repo,
 			   struct config_location_options *opts,
 			   const char *prefix);
diff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh
index f7a74ddc2c..40f6f90ef2 100755
--- a/t/t1312-config-batch.sh
+++ b/t/t1312-config-batch.sh
@@ -47,9 +47,10 @@ test_expect_success 'help command' '
 	echo "help 1" >in &&
 
 	cat >expect <<-\EOF &&
-	help 1 count 2
+	help 1 count 3
 	help 1 help 1
 	help 1 get 1
+	help 1 set 1
 	EOF
 
 	git config-batch >out <in &&
@@ -63,9 +64,10 @@ test_expect_success 'help -z' '
 	EOF
 
 	cat >expect <<-\EOF &&
-	4:help 1:1 5:count 1:2
+	4:help 1:1 5:count 1:3
 	4:help 1:1 4:help 1:1
 	4:help 1:1 3:get 1:1
+	4:help 1:1 3:set 1:1
 	15:unknown_command
 	EOF
 
@@ -205,4 +207,92 @@ test_expect_success 'get config with -z' '
 	test_cmp expect out
 '
 
+test_expect_success 'set config by scope' '
+	test_when_finished git config remove-section test.set &&
+	GIT_CONFIG_SYSTEM=system-config-file &&
+	GIT_CONFIG_NOSYSTEM=0 &&
+	GIT_CONFIG_GLOBAL=global-config-file &&
+	export GIT_CONFIG_SYSTEM &&
+	export GIT_CONFIG_NOSYSTEM &&
+	export GIT_CONFIG_GLOBAL &&
+
+	cat >in <<-\EOF &&
+	set 1 system test.set.system system
+	set 1 global test.set.global global
+	set 1 local test.set.local local with spaces
+	set 1 worktree test.set.worktree worktree
+	set 1 submodule test.set.submodule submodule
+	set 1 command test.set.command command
+	set 1 inherited test.set.inherited inherited
+	EOF
+
+	cat >expect <<-\EOF &&
+	set 1 success system test.set.system system
+	set 1 success global test.set.global global
+	set 1 success local test.set.local local with spaces
+	set 1 success worktree test.set.worktree worktree
+	command_parse_error set
+	command_parse_error set
+	command_parse_error set
+	EOF
+
+	git config-batch <in >out 2>err &&
+
+	test_must_be_empty err &&
+	test_cmp expect out &&
+
+	cat >expect-values <<-EOF &&
+	file:system-config-file	system
+	file:global-config-file	global
+	file:.git/config	local with spaces
+	file:.git/config.worktree	worktree
+	EOF
+
+	git config get --show-origin --regexp --all test.set.* >values &&
+	test_cmp expect-values values
+'
+
+test_expect_success 'set config by scope with -z' '
+	test_when_finished git config remove-section test.set &&
+	GIT_CONFIG_SYSTEM=system-config-file &&
+	GIT_CONFIG_NOSYSTEM=0 &&
+	GIT_CONFIG_GLOBAL=global-config-file &&
+	export GIT_CONFIG_SYSTEM &&
+	export GIT_CONFIG_NOSYSTEM &&
+	export GIT_CONFIG_GLOBAL &&
+
+	cat >in <<-\EOF &&
+	3:set NUL 1:1 NUL 6:system NUL 15:test.set.system NUL 6:system NUL NUL
+	3:set NUL 1:1 NUL 6:global NUL 15:test.set.global NUL 6:global NUL NUL
+	3:set NUL 1:1 NUL 5:local NUL 14:test.set.local NUL 17:local with spaces NUL NUL
+	3:set NUL 1:1 NUL 8:worktree NUL 17:test.set.worktree NUL 8:worktree NUL NUL
+	3:set NUL 1:1 NUL 9:submodule NUL 18:test.set.submodule NUL 9:submodule NUL NUL
+	3:set NUL 1:1 NUL 7:command NUL 16:test.set.command NUL 7:command NUL NUL
+	3:set NUL 1:1 NUL 9:inherited NUL 18:test.set.inherited NUL 9:inherited NUL NUL
+	EOF
+
+	cat >expect <<-\EOF &&
+	3:set 1:1 7:success 6:system 15:test.set.system 6:system
+	3:set 1:1 7:success 6:global 15:test.set.global 6:global
+	3:set 1:1 7:success 5:local 14:test.set.local 17:local with spaces
+	3:set 1:1 7:success 8:worktree 17:test.set.worktree 8:worktree
+	19:command_parse_error 3:set
+	19:command_parse_error 3:set
+	19:command_parse_error 3:set
+	EOF
+
+	test_zformat git config-batch -z >out <in &&
+	test_cmp expect out &&
+
+	cat >expect-values <<-EOF &&
+	file:system-config-file	system
+	file:global-config-file	global
+	file:.git/config	local with spaces
+	file:.git/config.worktree	worktree
+	EOF
+
+	git config get --show-origin --regexp --all test.set.* >values &&
+	test_cmp expect-values values
+'
+
 test_done
-- 
gitgitgadget