Subject: [PATCH 02/11] config-batch: create parse loop and unknown command

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

From: Derrick Stolee <[email-masked]>

As we build new features in the config-batch command, we define the
plaintext protocol with line-by-line output and responses. To think to the
future, we make sure that the protocol has a clear way to respond to an
unknown command or an unknown version of that command.

As some commands will allow the final argument to contain spaces or even be
able to parse "\ " as a non-split token, we only provide the remaining line
as data.

Signed-off-by: Derrick Stolee <[email-masked]>
---
 Documentation/git-config-batch.adoc |  23 ++++-
 builtin/config-batch.c              | 133 +++++++++++++++++++++++++++-
 t/t1312-config-batch.sh             |  19 +++-
 3 files changed, 170 insertions(+), 5 deletions(-)

diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc
index dfa0bd83e2..9ca04b0c1e 100644
--- a/Documentation/git-config-batch.adoc
+++ b/Documentation/git-config-batch.adoc
@@ -13,7 +13,28 @@ SYNOPSIS
 
 DESCRIPTION
 -----------
-TODO
+Tools frequently need to change their behavior based on values stored in
+Git's configuration files. These files may have complicated conditions
+for including extra files, so it is difficult to produce an independent
+parser. To avoid executing multiple processes to discover or modify
+multiple configuration values, the `git config-batch` command allows a
+single process to handle multiple requests using a machine-parseable
+interface across `stdin` and `stdout`.
+
+PROTOCOL
+--------
+By default, the protocol uses line feeds (`LF`) to signal the end of a
+command over `stdin` or a response over `stdout`.
+
+The protocol will be extended in the future, and consumers should be
+resilient to older Git versions not understanding the latest command
+set. Thus, if the Git version includes the `git config-batch` builtin
+but doesn't understand an input command, it will return a single line
+response:
+
+```
+unknown_command LF
+```
 
 SEE ALSO
 --------
diff --git a/builtin/config-batch.c b/builtin/config-batch.c
index ea4f408ecb..dffedb8ca2 100644
--- a/builtin/config-batch.c
+++ b/builtin/config-batch.c
@@ -3,17 +3,144 @@
 #include "config.h"
 #include "environment.h"
 #include "parse-options.h"
+#include "strbuf.h"
+#include "string-list.h"
 
 static const char *const builtin_config_batch_usage[] = {
 	N_("git config-batch <options>"),
 	NULL
 };
 
+#define UNKNOWN_COMMAND "unknown_command"
+
+static int emit_response(const char *response, ...)
+{
+	va_list params;
+	const char *token;
+
+	printf("%s", response);
+
+	va_start(params, response);
+	while ((token = va_arg(params, const char *)))
+		printf(" %s", token);
+	va_end(params);
+
+	printf("\n");
+	fflush(stdout);
+	return 0;
+}
+
+/**
+ * A function pointer type for defining a command. The function is
+ * responsible for handling different versions of the command name.
+ *
+ * Provides the remaining 'data' for the command, to be parsed by
+ * the function as needed according to its parsing rules.
+ *
+ * These functions should only return a negative value if they result
+ * in such a catastrophic failure that the process should end.
+ *
+ * Return 0 on success.
+ */
+typedef int (*command_fn)(struct repository *repo,
+			  char *data, size_t data_len);
+
+static int unknown_command(struct repository *repo UNUSED,
+			  char *data UNUSED, size_t data_len UNUSED)
+{
+	return emit_response(UNKNOWN_COMMAND, NULL);
+}
+
+struct command {
+	const char *name;
+	command_fn fn;
+	int version;
+};
+
+static struct command commands[] = {
+	/* unknown_command must be last. */
+	{
+		.name = "",
+		.fn   = unknown_command,
+	},
+};
+
+#define COMMAND_COUNT ((size_t)(sizeof(commands) / sizeof(*commands)))
+
+/**
+ * Process a single line from stdin and process the command.
+ *
+ * Returns 0 on successful processing of command, including the
+ * unknown_command output.
+ *
+ * Returns 1 on natural exit due to exist signal of empty line.
+ *
+ * Returns negative value on other catastrophic error.
+ */
+static int process_command(struct repository *repo)
+{
+	static struct strbuf line = STRBUF_INIT;
+	struct string_list tokens = STRING_LIST_INIT_NODUP;
+	const char *command;
+	int version;
+	char *data = NULL;
+	size_t data_len = 0;
+	int res = 0;
+
+	strbuf_getline(&line, stdin);
+
+	if (!line.len)
+		return 1;
+
+	/* Parse out the first two tokens, command and version. */
+	string_list_split_in_place(&tokens, line.buf, " ", 2);
+
+	if (tokens.nr < 2) {
+		res = error(_("expected at least 2 tokens, got %"PRIu32),
+			    (uint32_t)tokens.nr);
+		goto cleanup;
+	}
+
+	command = tokens.items[0].string;
+
+	if (!git_parse_int(tokens.items[1].string, &version)) {
+		res = error(_("unable to parse '%s' to integer"),
+			    tokens.items[1].string);
+		goto cleanup;
+	}
+
+	if (tokens.nr >= 3) {
+		data = tokens.items[2].string;
+		data_len = strlen(tokens.items[2].string);
+	}
+
+	for (size_t i = 0; i < COMMAND_COUNT; i++) {
+		/*
+		 * Run the ith command if we have hit the unknown
+		 * command or if the name and version match.
+		 */
+		if (!commands[i].name[0] ||
+		    (!strcmp(command, commands[i].name) &&
+		     commands[i].version == version)) {
+			res = commands[i].fn(repo, data, data_len);
+			goto cleanup;
+		}
+	}
+
+	BUG(_("scanned to end of command list, including 'unknown_command'"));
+
+cleanup:
+	strbuf_reset(&line);
+	string_list_clear(&tokens, 0);
+	return res;
+}
+
 int cmd_config_batch(int argc,
 		     const char **argv,
 		     const char *prefix,
 		     struct repository *repo)
 {
+	int res = 0;
 	struct option options[] = {
 		OPT_END(),
 	};
@@ -26,5 +153,9 @@ int cmd_config_batch(int argc,
 
 	repo_config(repo, git_default_config, NULL);
 
-	return 0;
+	while (!(res = process_command(repo)));
+
+	if (res == 1)
+		return 0;
+	die(_("an unrecoverable error occurred during command execution"));
 }
diff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh
index f59ba4a0f3..f60ef35e38 100755
--- a/t/t1312-config-batch.sh
+++ b/t/t1312-config-batch.sh
@@ -4,9 +4,22 @@ test_description='Test git config-batch'
 
 . ./test-lib.sh
 
-test_expect_success 'help text' '
-	test_must_fail git config-batch -h >out &&
-	grep usage out
+test_expect_success 'no commands' '
+	echo | git config-batch >out &&
+	test_must_be_empty out
+'
+
+test_expect_success 'unknown_command' '
+	echo unknown_command >expect &&
+	echo "bogus 1 line of tokens" >in &&
+	git config-batch >out <in &&
+	test_cmp expect out
+'
+
+test_expect_success 'failed to parse version' '
+	echo "bogus BAD_VERSION line of tokens" >in &&
+	test_must_fail git config-batch 2>err <in &&
+	test_grep BAD_VERSION err
 '
 
 test_done
-- 
gitgitgadget