Subject: [PATCH 11/11] config-batch: add unset v1 command

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

From: Derrick Stolee <[email-masked]>

Add a new 'unset' command with version 1 that mimics 'git config
--unset' with optional regex pattern or '--fixed-value' arguments.

Signed-off-by: Derrick Stolee <[email-masked]>
---
 Documentation/git-config-batch.adoc | 28 ++++++++
 builtin/config-batch.c              | 99 +++++++++++++++++++++++++++++
 t/t1312-config-batch.sh             | 61 ++++++++++++++++--
 3 files changed, 181 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc
index feec85c4ef..bdfd872d65 100644
--- a/Documentation/git-config-batch.adoc
+++ b/Documentation/git-config-batch.adoc
@@ -135,6 +135,34 @@ set 1 success <scope> <key> <value>
 set 1 failed <scope> <key> <value>
 ------------
 
+`unset` version 1::
+	The `unset` command removes a single value from 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. There could be two additional
+	arguments used to match specific config values, where the first
+	is either `arg:regex` or `arg:fixed-value` to specify the type
+	of match.
++
+------------
+unset 1 <scope> <key>
+unset 1 <scope> <key> arg:regex <value-pattern>
+unset 1 <scope> <key> arg:fixed-value <value>
+------------
++
+These uses will match the behavior of `git config --unset --<scope> <key>`
+with the additional arguments of `<value-pattern>` if `arg:regex` is
+given or `--fixed-value <value>` if `arg:fixed-value` is given.
++
+The response of these commands will include a `success` message
+if matched values are found and removed as expected or `failed` if an
+unexpected failure occurs:
++
+------------
+unset 1 success <scope> <key>
+unset 1 failed <scope> <key>
+------------
+
 NUL-Terminated Format
 ~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/builtin/config-batch.c b/builtin/config-batch.c
index 373b0cad47..25a942ba61 100644
--- a/builtin/config-batch.c
+++ b/builtin/config-batch.c
@@ -17,6 +17,7 @@ static int zformat = 0;
 #define HELP_COMMAND "help"
 #define GET_COMMAND "get"
 #define SET_COMMAND "set"
+#define UNSET_COMMAND "unset"
 #define COMMAND_PARSE_ERROR "command_parse_error"
 
 static void print_word(const char *word, int start)
@@ -445,6 +446,99 @@ cleanup:
 	return res;
 }
 
+/**
+ * 'unset' command, version 1.
+ *
+ * Positional arguments should be of the form:
+ *
+ * [0] scope ("system", "global", "local", or "worktree")
+ * [1] config key
+ * [2] config value
+ * [3*] match ("regex", "fixed-value")
+ * [4*] value regex OR value string
+ *
+ * [N*] indicates optional parameters that are not needed.
+ */
+static int unset_command_1(struct repository *repo,
+			 const char *prefix,
+			 char *data,
+			 size_t data_len)
+{
+	int res = 0, err = 0, flags = 0;
+	enum config_scope scope = CONFIG_SCOPE_UNKNOWN;
+	char *token = NULL, *key = NULL, *value_pattern = NULL;
+	size_t token_len;
+	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;
+
+	token_len = parse_token(&data, &data_len, &token, &err);
+	if (err)
+		goto parse_error;
+
+	if (token_len && !strncmp(token, "arg:", 4)) {
+		if (!strcmp(token + 4, "fixed-value"))
+			flags |= CONFIG_FLAGS_FIXED_VALUE;
+		/* no special logic for arg:regex. */
+		else if (strcmp(token + 4, "regex"))
+			goto parse_error; /* unknown arg. */
+
+		/* Use the remaining data as the value string. */
+		if (!zformat)
+			value_pattern = data;
+		else {
+			parse_token(&data, &data_len, &value_pattern, &err);
+			if (err)
+				goto parse_error;
+		}
+	} else if (token_len) {
+		/*
+		 * If we have remaining tokens not starting in "arg:",
+		 * then we don't understand them.
+		 */
+		goto parse_error;
+	}
+
+	if (location_options_set_scope(&locopts, scope))
+		goto parse_error;
+	location_options_init(repo, &locopts, prefix);
+
+	res = repo_config_set_multivar_in_file_gently(
+			repo,
+			locopts.source.file,
+			key,
+			/* value */ NULL,
+			value_pattern,
+			/* comment */ NULL,
+			flags);
+
+	if (res)
+		res = emit_response(UNSET_COMMAND, "1", "failure",
+				    scope_str(scope), key, NULL);
+	else
+		res = emit_response(UNSET_COMMAND, "1", "success",
+				    scope_str(scope), key, NULL);
+
+	goto cleanup;
+
+parse_error:
+	res = command_parse_error(UNSET_COMMAND);
+
+cleanup:
+	location_options_release(&locopts);
+	return res;
+}
+
 struct command {
 	const char *name;
 	command_fn fn;
@@ -467,6 +561,11 @@ static struct command commands[] = {
 		.fn = set_command_1,
 		.version = 1,
 	},
+	{
+		.name = UNSET_COMMAND,
+		.fn = unset_command_1,
+		.version = 1,
+	},
 	/* unknown_command must be last. */
 	{
 		.name = "",
diff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh
index 11380f4247..3bddbc0de3 100755
--- a/t/t1312-config-batch.sh
+++ b/t/t1312-config-batch.sh
@@ -47,10 +47,11 @@ test_expect_success 'help command' '
 	echo "help 1" >in &&
 
 	cat >expect <<-\EOF &&
-	help 1 count 3
+	help 1 count 4
 	help 1 help 1
 	help 1 get 1
 	help 1 set 1
+	help 1 unset 1
 	EOF
 
 	git config-batch >out <in &&
@@ -64,10 +65,11 @@ test_expect_success 'help -z' '
 	EOF
 
 	cat >expect <<-\EOF &&
-	4:help 1:1 5:count 1:3
+	4:help 1:1 5:count 1:4
 	4:help 1:1 4:help 1:1
 	4:help 1:1 3:get 1:1
 	4:help 1:1 3:set 1:1
+	4:help 1:1 5:unset 1:1
 	15:unknown_command
 	EOF
 
@@ -295,15 +297,60 @@ test_expect_success 'set config by scope with -z' '
 	test_cmp expect-values values
 '
 
-test_expect_success 'read/write interactions in sequence' '
-	test_when_finished git config remove-section test.rw &&
+test_expect_success 'unset config by scope and filter' '
+	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.unset.key system
+	set 1 global test.unset.key global
+	set 1 local test.unset.key local with spaces
+	set 1 worktree test.unset.key worktree
+	unset 1 system test.unset.key
+	unset 1 global test.unset.key arg:regex g.*
+	unset 1 local test.unset.key arg:fixed-value local with spaces
+	unset 1 worktree test.unset.key arg:fixed-value submodule
+	unset 1 worktree test.unset.key arg:regex l.*
+	EOF
+
+	cat >expect <<-\EOF &&
+	set 1 success system test.unset.key system
+	set 1 success global test.unset.key global
+	set 1 success local test.unset.key local with spaces
+	set 1 success worktree test.unset.key worktree
+	unset 1 success system test.unset.key
+	unset 1 success global test.unset.key
+	unset 1 success local test.unset.key
+	unset 1 failure worktree test.unset.key
+	unset 1 failure worktree test.unset.key
+	EOF
+
+	git config-batch <in >out 2>err &&
 
+	test_must_be_empty err &&
+	test_cmp expect out &&
+
+	cat >expect-values <<-EOF &&
+	file:.git/config.worktree	worktree
+	EOF
+
+	git config get --show-origin --regexp --all test.unset.key >values &&
+	test_cmp expect-values values
+'
+
+test_expect_success 'read/write interactions in sequence' '
 	cat >in <<-\EOF &&
 	get 1 local test.rw.missing
 	set 1 local test.rw.found found
 	get 1 local test.rw.found
 	set 1 local test.rw.found updated
 	get 1 local test.rw.found
+	unset 1 local test.rw.found arg:fixed-value updated
+	get 1 local test.rw.found
 	EOF
 
 	cat >expect <<-\EOF &&
@@ -312,14 +359,14 @@ test_expect_success 'read/write interactions in sequence' '
 	get 1 found test.rw.found local found
 	set 1 success local test.rw.found updated
 	get 1 found test.rw.found local updated
+	unset 1 success local test.rw.found
+	get 1 missing test.rw.found
 	EOF
 
 	git config-batch <in >out 2>err &&
 
 	test_must_be_empty err &&
-	test_cmp expect out &&
-
-	test_cmp_config updated test.rw.found
+	test_cmp expect out
 '
 
 test_done
-- 
gitgitgadget