git/list[1] front-page[2] threads[3] people[4] search[5] about
 

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

From
Derrick Stolee via GitGitGadget <gitgitgadget@gmail.com>
Date
Feb 4, 2026, 14:19 UTC
Message-ID
<ecd26a0f1fad5615aea07a388e34f02e9f33b870.1770214803.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.2033.git.1770214803.gitgitgadget@gmail.com>
From: Derrick Stolee <stolee@gmail.com>

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 <stolee@gmail.com>
---
 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
Previous: Jean-Noël AvilaNext: Junio C Hamano
Message 8 of 40 in “[RFC] config-batch: a new builtin for tools querying config”
  1. 00/11 [RFC] config-batch: a new builtin for tools querying configDerrick Stolee via GitGitGadget, Feb 4, 2026
  2. 01/11 config-batch: basic boilerplate of new builtinDerrick Stolee via GitGitGadget, Feb 4, 2026
  3. Junio C HamanoFeb 4, 2026
  4. Derrick StoleeFeb 5, 2026
  5. Kristoffer HaugsbakkFeb 5, 2026
  6. Kristoffer HaugsbakkFeb 5, 2026
  7. Jean-Noël AvilaFeb 6, 2026
  8. 02/11 config-batch: create parse loop and unknown commandDerrick Stolee via GitGitGadget, Feb 4, 2026
  9. Junio C HamanoFeb 4, 2026
  10. Kristoffer HaugsbakkFeb 5, 2026
  11. Jean-Noël AvilaFeb 6, 2026
  12. 03/11 config-batch: implement get v1Derrick Stolee via GitGitGadget, Feb 4, 2026
  13. Jean-Noël AvilaFeb 6, 2026
  14. 04/11 config-batch: create 'help' commandDerrick Stolee via GitGitGadget, Feb 4, 2026
  15. Jean-Noël AvilaFeb 6, 2026
  16. Derrick StoleeFeb 10, 2026
  17. 05/11 config-batch: add NUL-terminated I/O formatDerrick Stolee via GitGitGadget, Feb 4, 2026
  18. Kristoffer HaugsbakkFeb 5, 2026
  19. Jean-Noël AvilaFeb 6, 2026
  20. 06/11 docs: add design doc for config-batchDerrick Stolee via GitGitGadget, Feb 4, 2026
  21. Kristoffer HaugsbakkFeb 5, 2026
  22. Derrick StoleeFeb 10, 2026
  23. 07/11 config: extract location structs from builtinDerrick Stolee via GitGitGadget, Feb 4, 2026
  24. 08/11 config-batch: pass prefix through commandsDerrick Stolee via GitGitGadget, Feb 4, 2026
  25. 09/11 config-batch: add 'set' v1 commandDerrick Stolee via GitGitGadget, Feb 4, 2026
  26. Kristoffer HaugsbakkFeb 5, 2026
  27. Kristoffer HaugsbakkFeb 5, 2026
  28. Kristoffer HaugsbakkFeb 5, 2026
  29. Derrick StoleeFeb 10, 2026
  30. Jean-Noël AvilaFeb 6, 2026
  31. 10/11 t1312: create read/write testDerrick Stolee via GitGitGadget, Feb 4, 2026
  32. 11/11 config-batch: add unset v1 commandDerrick Stolee via GitGitGadget, Feb 4, 2026
  33. Kristoffer HaugsbakkFeb 5, 2026
  34. Junio C HamanoFeb 4, 2026
  35. Derrick StoleeFeb 5, 2026
  36. brian m. carlsonFeb 5, 2026
  37. Derrick StoleeFeb 5, 2026
  38. Derrick StoleeFeb 10, 2026
  39. Phillip WoodFeb 5, 2026
  40. Kristoffer HaugsbakkFeb 5, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.