{"thread":{"id":"64916","subject":"[PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","startedAt":"2026-02-04T14:20:06Z","lastAt":"2026-02-10T04:49:14Z","messageCount":40,"participants":["Derrick Stolee via GitGitGadget","Junio C Hamano","brian m. carlson","Derrick Stolee","Phillip Wood","Kristoffer Haugsbakk","Jean-Noël Avila"],"isPatch":true,"patchVersion":1,"patchTotal":11},"messages":[{"id":"535149","messageId":"pull.2033.git.1770214803.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":null,"subject":"[PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:19:52Z","receivedAt":"2026-02-04T14:20:06Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"This RFC explores a new git config-batch builtin that allows tools to\ninteract with Git's config data with multiple queries using a single\nprocess. This is an orthogonal alternative to the effort to create a stable,\nlinkable config API. Both approaches have different strengths.\n\nMy main motivation is the performance of git-credential-manager on Windows\nplatforms as it can call git config get dozens of times. At 150-200ms per\nexecution, that adds up significantly, leading to multiple seconds just to\nload a credential that already exists. I believe that there are other\nbenefits to having this interface available, but I can't recall any\nspecifics at the moment.\n\nThis RFC adds git config-batch with a protocol over stdin/stdout for\nexecuting multiple config queries. The implementation has a limited set of\npotential queries, but also creates a model for compatibility for tools to\nautomatically adapt to different Git versions.\n\nI'm submitting this as an RFC before I've polished all of the details\nbecause I want to make sure I'm going down a good direction. Please focus\nfeedback in these questions:\n\n * Is this a worthwhile feature to add to Git?\n * Is this a reasonable protocol for stdin/stdout?\n * How can we structure the code to make it easier to contribute new\n   commands in the future?\n * This seems like a place where parallel contributions can be made once the\n   baseline is implemented. Is there interest in further contributions to\n   expand the commands?\n\nThis RFC adds the following commands over stdin:\n\n 1. help lists the available commands, giving the caller an understanding of\n    what is available in this Git version.\n 2. get loads a value for a given key within a certain scope, with optional\n    value patterns.\n 3. set assigns a key-value pair in a given scope.\n 4. unset removes a key-value pair in a given scope with optional value\n    patterns.\n\nEach command has an associated version, in case we need to expand or alter\nthe functionality in the future. This includes the potential to deprecate\nand remove certain versions that we no longer want to support, such as\nreplacing set version 1 with a version 2 and making version 1 no longer\navailable. I do hope that we will mostly be able to move with new command\nnames, such as a set-all command including the options for git config set\n--all ... instead of increasing the version of the set command.\n\nThere is a -z option that changes the command interface to use\nNUL-terminated strings. Two NULs specify a command boundary, which promotes\ncompatibility with a caller that sends an unknown command. However, this\nmeans that we cannot specify an empty string as a token within a command\nunless we add more data. This format uses <N>:<string> to provide the\ninteger <N> which specifies the length of <string>. This is a little\ncumbersome, but the format is intended for tools, not humans.\n\nI have a test integration with git-credential-manager available [1] for\ntesting. This includes a model for interacting with git config-batch in a\ncompatible way that will respond to certain features not being available:\n\n 1. If git config-batch fails immediately, then all queries are handled by\n    git config.\n 2. If git config-batch starts without failure, then the first query is for\n    the help command.\n 3. As queries come to the config system, the query is checked against the\n    available commands advertised by git config-batch. If the appropriate\n    command is available, then the query is made in that process. If not,\n    then the query uses the existing git config command.\n\nOne thing that I think would be valuable to include is a reload command that\nsignals that the git config-batch process should reload the configset into\nmemory due to config manipulations in other processes, especially while git\nconfig-batch doesn't have all capabilities from git config. I'll include\nthat in the first version for review, if this RFC leads to positive support.\n\n[1] https://github.com/git-ecosystem/git-credential-manager/pull/2245\n\nI have a few concerns with this implementation that I'd like to improve\nbefore submitting a version for full review. I list them here so you can see\nthe flaws that I already see, but also so you can add to this list:\n\n * We need a reload command (as mentioned above).\n * The tests need to include a submodule and submodule-level config.\n * When specifying the local scope to the get command, the matched value\n   does not include worktree or submodule config in the same way that git\n   config get --local <key> would.\n * The token-parsing API in this helper is still too complicated to use. I\n   should create parsing tooling similar to the parse-opts API so each\n   command could specify its use of positional values and optional\n   arguments.\n * The use of arg:<arg> to specify an optional argument creates the\n   inability to submit a value that starts with arg:. Consider alternative\n   ways to specify arguments or to specify that the remaining data in the\n   command (including spaces) is a final positional argument.\n * In general, I found myself implementing behavior based on the deprecated\n   forms of git config that use the --get or --unset style arguments instead\n   of git config (set|unset|get) subcommands. It's worth making sure that\n   any references to equivalent git config commands use the new modes.\n * I need to add an --[no-]includes option as a command-line argument that\n   signals whether include sections should be followed. I don't believe this\n   should be specified on a per-command basis, but I'm open to suggestions.\n * I have an early draft of a technical document detailing the plan for this\n   builtin. It has some lists of intended future commands that have not been\n   implemented. This would also be a good place to document any parsing APIs\n   built to help contributors adding to this builtin.\n\nThanks, -Stolee\n\nDerrick Stolee (11):\n  config-batch: basic boilerplate of new builtin\n  config-batch: create parse loop and unknown command\n  config-batch: implement get v1\n  config-batch: create 'help' command\n  config-batch: add NUL-terminated I/O format\n  docs: add design doc for config-batch\n  config: extract location structs from builtin\n  config-batch: pass prefix through commands\n  config-batch: add 'set' v1 command\n  t1312: create read/write test\n  config-batch: add unset v1 command\n\n .gitignore                                |   1 +\n Documentation/git-config-batch.adoc       | 214 ++++++\n Documentation/meson.build                 |   1 +\n Documentation/technical/config-batch.adoc |  70 ++\n Makefile                                  |   1 +\n builtin.h                                 |   7 +\n builtin/config-batch.c                    | 772 ++++++++++++++++++++++\n builtin/config.c                          | 117 +---\n command-list.txt                          |   1 +\n config.c                                  | 116 ++++\n config.h                                  |  26 +\n git.c                                     |   1 +\n meson.build                               |   1 +\n t/meson.build                             |   1 +\n t/t1312-config-batch.sh                   | 372 +++++++++++\n 15 files changed, 1592 insertions(+), 109 deletions(-)\n create mode 100644 Documentation/git-config-batch.adoc\n create mode 100644 Documentation/technical/config-batch.adoc\n create mode 100644 builtin/config-batch.c\n create mode 100755 t/t1312-config-batch.sh\n\n\nbase-commit: 83a69f19359e6d9bc980563caca38b2b5729808c\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-2033%2Fderrickstolee%2Fbatched-config-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2033/derrickstolee/batched-config-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/2033\n-- \ngitgitgadget\n"},{"id":"535150","messageId":"c4dab0609613bc5d43bce705dca2f057674a5d5b.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 01/11] config-batch: basic boilerplate of new builtin","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:19:53Z","receivedAt":"2026-02-04T14:20:09Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nLater changes will document, implement, and test this new builtin. For now,\nthis serves as the latest example of the minimum boilerplate to introduce a\nnew builtin.\n\nRecently, we updated the comment in builtin.h about how to create a new\nbuiltin, but failed to mention the required change to meson.build files for\nsome CI builds to pass. Fix that oversight.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n .gitignore                          |  1 +\n Documentation/git-config-batch.adoc | 24 +++++++++++++++++++++++\n Documentation/meson.build           |  1 +\n Makefile                            |  1 +\n builtin.h                           |  7 +++++++\n builtin/config-batch.c              | 30 +++++++++++++++++++++++++++++\n command-list.txt                    |  1 +\n git.c                               |  1 +\n meson.build                         |  1 +\n t/meson.build                       |  1 +\n t/t1312-config-batch.sh             | 12 ++++++++++++\n 11 files changed, 80 insertions(+)\n create mode 100644 Documentation/git-config-batch.adoc\n create mode 100644 builtin/config-batch.c\n create mode 100755 t/t1312-config-batch.sh\n\ndiff --git a/.gitignore b/.gitignore\nindex 78a45cb5be..42640b5e24 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -44,6 +44,7 @@\n /git-commit-graph\n /git-commit-tree\n /git-config\n+/git-config-batch\n /git-count-objects\n /git-credential\n /git-credential-cache\ndiff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\nnew file mode 100644\nindex 0000000000..dfa0bd83e2\n--- /dev/null\n+++ b/Documentation/git-config-batch.adoc\n@@ -0,0 +1,24 @@\n+git-config-batch(1)\n+===================\n+\n+NAME\n+----\n+git-config-batch - Get and set options using machine-parseable interface\n+\n+\n+SYNOPSIS\n+--------\n+[verse]\n+'git config-batch' <options>\n+\n+DESCRIPTION\n+-----------\n+TODO\n+\n+SEE ALSO\n+--------\n+linkgit:git-config[1]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex f02dbc20cb..f5ad117921 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -29,6 +29,7 @@ manpages = {\n   'git-commit-tree.adoc' : 1,\n   'git-commit.adoc' : 1,\n   'git-config.adoc' : 1,\n+  'git-config-batch.adoc' : 1,\n   'git-count-objects.adoc' : 1,\n   'git-credential-cache--daemon.adoc' : 1,\n   'git-credential-cache.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 8aa489f3b6..aa3868e513 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1390,6 +1390,7 @@ BUILTIN_OBJS += builtin/commit-graph.o\n BUILTIN_OBJS += builtin/commit-tree.o\n BUILTIN_OBJS += builtin/commit.o\n BUILTIN_OBJS += builtin/config.o\n+BUILTIN_OBJS += builtin/config-batch.o\n BUILTIN_OBJS += builtin/count-objects.o\n BUILTIN_OBJS += builtin/credential-cache--daemon.o\n BUILTIN_OBJS += builtin/credential-cache.o\ndiff --git a/builtin.h b/builtin.h\nindex e5e16ecaa6..5f5a19635e 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -68,12 +68,18 @@\n  *\n  * . Add `builtin/foo.o` to `BUILTIN_OBJS` in `Makefile`.\n  *\n+ * . Add 'builtin/foo.c' to the 'builtin_sources' array in 'meson.build'.\n+ *\n  * Additionally, if `foo` is a new command, there are 4 more things to do:\n  *\n  * . Add tests to `t/` directory.\n  *\n+ * . Add the test script to 'integration_tests' in  't/meson.build'.\n+ *\n  * . Write documentation in `Documentation/git-foo.adoc`.\n  *\n+ * . Add 'git-foo.adoc' to the manpages list in 'Documentation/meson.build'.\n+ *\n  * . Add an entry for `git-foo` to `command-list.txt`.\n  *\n  * . Add an entry for `/git-foo` to `.gitignore`.\n@@ -167,6 +173,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix, struct repositor\n int cmd_commit_graph(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_commit_tree(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_config(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_config_batch(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_count_objects(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_credential(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_credential_cache(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/config-batch.c b/builtin/config-batch.c\nnew file mode 100644\nindex 0000000000..ea4f408ecb\n--- /dev/null\n+++ b/builtin/config-batch.c\n@@ -0,0 +1,30 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+#include \"builtin.h\"\n+#include \"config.h\"\n+#include \"environment.h\"\n+#include \"parse-options.h\"\n+\n+static const char *const builtin_config_batch_usage[] = {\n+\tN_(\"git config-batch <options>\"),\n+\tNULL\n+};\n+\n+int cmd_config_batch(int argc,\n+\t\t     const char **argv,\n+\t\t     const char *prefix,\n+\t\t     struct repository *repo)\n+{\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\tshow_usage_with_options_if_asked(argc, argv,\n+\t\t\t\t\t builtin_config_batch_usage, options);\n+\n+\targc = parse_options(argc, argv, prefix, options, builtin_config_batch_usage,\n+\t\t\t     0);\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex accd3d0c4b..57c7c7458d 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -83,6 +83,7 @@ git-commit                              mainporcelain           history\n git-commit-graph                        plumbingmanipulators\n git-commit-tree                         plumbingmanipulators\n git-config                              ancillarymanipulators           complete\n+git-config-batch                        plumbinginterrogators\n git-count-objects                       ancillaryinterrogators\n git-credential                          purehelpers\n git-credential-cache                    purehelpers\ndiff --git a/git.c b/git.c\nindex c5fad56813..6b55a867dd 100644\n--- a/git.c\n+++ b/git.c\n@@ -557,6 +557,7 @@ static struct cmd_struct commands[] = {\n \t{ \"commit-graph\", cmd_commit_graph, RUN_SETUP },\n \t{ \"commit-tree\", cmd_commit_tree, RUN_SETUP },\n \t{ \"config\", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG },\n+\t{ \"config-batch\", cmd_config_batch, RUN_SETUP_GENTLY },\n \t{ \"count-objects\", cmd_count_objects, RUN_SETUP },\n \t{ \"credential\", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"credential-cache\", cmd_credential_cache },\ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..040bc32c2d 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -582,6 +582,7 @@ builtin_sources = [\n   'builtin/commit-tree.c',\n   'builtin/commit.c',\n   'builtin/config.c',\n+  'builtin/config-batch.c',\n   'builtin/count-objects.c',\n   'builtin/credential-cache--daemon.c',\n   'builtin/credential-cache.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..0e9f1826f8 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -186,6 +186,7 @@ integration_tests = [\n   't1309-early-config.sh',\n   't1310-config-default.sh',\n   't1311-config-optional.sh',\n+  't1312-config-batch.sh',\n   't1350-config-hooks-path.sh',\n   't1400-update-ref.sh',\n   't1401-symbolic-ref.sh',\ndiff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh\nnew file mode 100755\nindex 0000000000..f59ba4a0f3\n--- /dev/null\n+++ b/t/t1312-config-batch.sh\n@@ -0,0 +1,12 @@\n+#!/bin/sh\n+\n+test_description='Test git config-batch'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'help text' '\n+\ttest_must_fail git config-batch -h >out &&\n+\tgrep usage out\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"535151","messageId":"ecd26a0f1fad5615aea07a388e34f02e9f33b870.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 02/11] config-batch: create parse loop and unknown command","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:19:54Z","receivedAt":"2026-02-04T14:20:10Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAs we build new features in the config-batch command, we define the\nplaintext protocol with line-by-line output and responses. To think to the\nfuture, we make sure that the protocol has a clear way to respond to an\nunknown command or an unknown version of that command.\n\nAs some commands will allow the final argument to contain spaces or even be\nable to parse \"\\ \" as a non-split token, we only provide the remaining line\nas data.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-config-batch.adoc |  23 ++++-\n builtin/config-batch.c              | 133 +++++++++++++++++++++++++++-\n t/t1312-config-batch.sh             |  19 +++-\n 3 files changed, 170 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\nindex dfa0bd83e2..9ca04b0c1e 100644\n--- a/Documentation/git-config-batch.adoc\n+++ b/Documentation/git-config-batch.adoc\n@@ -13,7 +13,28 @@ SYNOPSIS\n \n DESCRIPTION\n -----------\n-TODO\n+Tools frequently need to change their behavior based on values stored in\n+Git's configuration files. These files may have complicated conditions\n+for including extra files, so it is difficult to produce an independent\n+parser. To avoid executing multiple processes to discover or modify\n+multiple configuration values, the `git config-batch` command allows a\n+single process to handle multiple requests using a machine-parseable\n+interface across `stdin` and `stdout`.\n+\n+PROTOCOL\n+--------\n+By default, the protocol uses line feeds (`LF`) to signal the end of a\n+command over `stdin` or a response over `stdout`.\n+\n+The protocol will be extended in the future, and consumers should be\n+resilient to older Git versions not understanding the latest command\n+set. Thus, if the Git version includes the `git config-batch` builtin\n+but doesn't understand an input command, it will return a single line\n+response:\n+\n+```\n+unknown_command LF\n+```\n \n SEE ALSO\n --------\ndiff --git a/builtin/config-batch.c b/builtin/config-batch.c\nindex ea4f408ecb..dffedb8ca2 100644\n--- a/builtin/config-batch.c\n+++ b/builtin/config-batch.c\n@@ -3,17 +3,144 @@\n #include \"config.h\"\n #include \"environment.h\"\n #include \"parse-options.h\"\n+#include \"strbuf.h\"\n+#include \"string-list.h\"\n \n static const char *const builtin_config_batch_usage[] = {\n \tN_(\"git config-batch <options>\"),\n \tNULL\n };\n \n+#define UNKNOWN_COMMAND \"unknown_command\"\n+\n+static int emit_response(const char *response, ...)\n+{\n+\tva_list params;\n+\tconst char *token;\n+\n+\tprintf(\"%s\", response);\n+\n+\tva_start(params, response);\n+\twhile ((token = va_arg(params, const char *)))\n+\t\tprintf(\" %s\", token);\n+\tva_end(params);\n+\n+\tprintf(\"\\n\");\n+\tfflush(stdout);\n+\treturn 0;\n+}\n+\n+/**\n+ * A function pointer type for defining a command. The function is\n+ * responsible for handling different versions of the command name.\n+ *\n+ * Provides the remaining 'data' for the command, to be parsed by\n+ * the function as needed according to its parsing rules.\n+ *\n+ * These functions should only return a negative value if they result\n+ * in such a catastrophic failure that the process should end.\n+ *\n+ * Return 0 on success.\n+ */\n+typedef int (*command_fn)(struct repository *repo,\n+\t\t\t  char *data, size_t data_len);\n+\n+static int unknown_command(struct repository *repo UNUSED,\n+\t\t\t  char *data UNUSED, size_t data_len UNUSED)\n+{\n+\treturn emit_response(UNKNOWN_COMMAND, NULL);\n+}\n+\n+struct command {\n+\tconst char *name;\n+\tcommand_fn fn;\n+\tint version;\n+};\n+\n+static struct command commands[] = {\n+\t/* unknown_command must be last. */\n+\t{\n+\t\t.name = \"\",\n+\t\t.fn   = unknown_command,\n+\t},\n+};\n+\n+#define COMMAND_COUNT ((size_t)(sizeof(commands) / sizeof(*commands)))\n+\n+/**\n+ * Process a single line from stdin and process the command.\n+ *\n+ * Returns 0 on successful processing of command, including the\n+ * unknown_command output.\n+ *\n+ * Returns 1 on natural exit due to exist signal of empty line.\n+ *\n+ * Returns negative value on other catastrophic error.\n+ */\n+static int process_command(struct repository *repo)\n+{\n+\tstatic struct strbuf line = STRBUF_INIT;\n+\tstruct string_list tokens = STRING_LIST_INIT_NODUP;\n+\tconst char *command;\n+\tint version;\n+\tchar *data = NULL;\n+\tsize_t data_len = 0;\n+\tint res = 0;\n+\n+\tstrbuf_getline(&line, stdin);\n+\n+\tif (!line.len)\n+\t\treturn 1;\n+\n+\t/* Parse out the first two tokens, command and version. */\n+\tstring_list_split_in_place(&tokens, line.buf, \" \", 2);\n+\n+\tif (tokens.nr < 2) {\n+\t\tres = error(_(\"expected at least 2 tokens, got %\"PRIu32),\n+\t\t\t    (uint32_t)tokens.nr);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tcommand = tokens.items[0].string;\n+\n+\tif (!git_parse_int(tokens.items[1].string, &version)) {\n+\t\tres = error(_(\"unable to parse '%s' to integer\"),\n+\t\t\t    tokens.items[1].string);\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (tokens.nr >= 3) {\n+\t\tdata = tokens.items[2].string;\n+\t\tdata_len = strlen(tokens.items[2].string);\n+\t}\n+\n+\tfor (size_t i = 0; i < COMMAND_COUNT; i++) {\n+\t\t/*\n+\t\t * Run the ith command if we have hit the unknown\n+\t\t * command or if the name and version match.\n+\t\t */\n+\t\tif (!commands[i].name[0] ||\n+\t\t    (!strcmp(command, commands[i].name) &&\n+\t\t     commands[i].version == version)) {\n+\t\t\tres = commands[i].fn(repo, data, data_len);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\tBUG(_(\"scanned to end of command list, including 'unknown_command'\"));\n+\n+cleanup:\n+\tstrbuf_reset(&line);\n+\tstring_list_clear(&tokens, 0);\n+\treturn res;\n+}\n+\n int cmd_config_batch(int argc,\n \t\t     const char **argv,\n \t\t     const char *prefix,\n \t\t     struct repository *repo)\n {\n+\tint res = 0;\n \tstruct option options[] = {\n \t\tOPT_END(),\n \t};\n@@ -26,5 +153,9 @@ int cmd_config_batch(int argc,\n \n \trepo_config(repo, git_default_config, NULL);\n \n-\treturn 0;\n+\twhile (!(res = process_command(repo)));\n+\n+\tif (res == 1)\n+\t\treturn 0;\n+\tdie(_(\"an unrecoverable error occurred during command execution\"));\n }\ndiff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh\nindex f59ba4a0f3..f60ef35e38 100755\n--- a/t/t1312-config-batch.sh\n+++ b/t/t1312-config-batch.sh\n@@ -4,9 +4,22 @@ test_description='Test git config-batch'\n \n . ./test-lib.sh\n \n-test_expect_success 'help text' '\n-\ttest_must_fail git config-batch -h >out &&\n-\tgrep usage out\n+test_expect_success 'no commands' '\n+\techo | git config-batch >out &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'unknown_command' '\n+\techo unknown_command >expect &&\n+\techo \"bogus 1 line of tokens\" >in &&\n+\tgit config-batch >out <in &&\n+\ttest_cmp expect out\n+'\n+\n+test_expect_success 'failed to parse version' '\n+\techo \"bogus BAD_VERSION line of tokens\" >in &&\n+\ttest_must_fail git config-batch 2>err <in &&\n+\ttest_grep BAD_VERSION err\n '\n \n test_done\n-- \ngitgitgadget\n\n"},{"id":"535152","messageId":"3de1bba3b10668f0200e27def9128571f51c1f68.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 03/11] config-batch: implement get v1","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:19:55Z","receivedAt":"2026-02-04T14:20:12Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'get' command for the 'git config-batch' builtin is the first command\nand is currently at version 1. It returns at most one value, the same as\n'git config --get <key>' with optional value-based filtering.\n\nThe documentation and tests detail the specifics of how to format requests\nof this format and how to parse the results.\n\nFuture versions could consider multi-valued responses or regex-based key\nmatching.\n\nFor the sake of incremental exploration of the potential in the 'git\nconfig-batch' command, this is the only implementation being presented in\nthe first patch series.\n\nFuture extensions could include a '-z' parameter that uses NUL bytes in the\ncommand and output format to allow for spaces or newlines in the input or\nnewlines in the output.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-config-batch.adoc |  53 +++++-\n builtin/config-batch.c              | 251 +++++++++++++++++++++++++++-\n config.h                            |   3 +\n t/t1312-config-batch.sh             | 101 +++++++++++\n 4 files changed, 405 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\nindex 9ca04b0c1e..31dd42f481 100644\n--- a/Documentation/git-config-batch.adoc\n+++ b/Documentation/git-config-batch.adoc\n@@ -32,9 +32,58 @@ set. Thus, if the Git version includes the `git config-batch` builtin\n but doesn't understand an input command, it will return a single line\n response:\n \n-```\n+------------\n unknown_command LF\n-```\n+------------\n+\n+These are the commands that are currently understood:\n+\n+`get` version 1::\n+\tThe `get` command searches the config key-value pairs within a\n+\tgiven `<scope>` for values that match the fixed `<key>` and\n+\tfilters the resulting value based on an optional `<value-filter>`.\n+\tThis can either be a regex or a fixed value. The command format\n+\tis one of the following formats:\n++\n+------------\n+get 1 <scope> <key>\n+get 1 <scope> <key> arg:regex <value-pattern>\n+get 1 <scope> <key> arg:fixed-value <value>\n+------------\n++\n+The `<scope>` value can be one of `inherited`, `system`, `global`,\n+`local`, `worktree`, `submodule`, or `command`. If `inherited`, then all\n+config key-value pairs will be considered regardless of scope. Otherwise,\n+only the given scope will be considered.\n++\n+If no optional arguments are given, then the value will not be filtered\n+by any pattern matching. If `arg:regex` is specified, then the rest of\n+the line is considered a single string, `<value-pattern>`, and is\n+interpreted as a regular expression for matching against stored values,\n+similar to specifying a value to `get config --get <key> \"<value-pattern>\"`.\n+If `arg:fixed-value` is specified, then the rest of the line is\n+considered a single string, `<value>`, and is checked for an exact\n+match against the key-value pairs, simmilar to `git config --get <key>\n+--fixed-value \"<value>\"`.\n++\n+At mmost one key-value pair is returned, that being the last key-value\n+pair in the standard config order by scope and sequence within each scope.\n++\n+If a key-value pair is found, then the following output is given:\n++\n+------------\n+get 1 found <key> <scope> <value>\n+------------\n++\n+If no matching key-value pair is found, then the following output is\n+given:\n++\n+------------\n+get 1 missing <key> [<value-pattern>|<value>]\n+------------\n++\n+where `<value-pattern>` or `<value>` is only supplied if provided in\n+the command.\n \n SEE ALSO\n --------\ndiff --git a/builtin/config-batch.c b/builtin/config-batch.c\nindex dffedb8ca2..5782004080 100644\n--- a/builtin/config-batch.c\n+++ b/builtin/config-batch.c\n@@ -12,6 +12,8 @@ static const char *const builtin_config_batch_usage[] = {\n };\n \n #define UNKNOWN_COMMAND \"unknown_command\"\n+#define GET_COMMAND \"get\"\n+#define COMMAND_PARSE_ERROR \"command_parse_error\"\n \n static int emit_response(const char *response, ...)\n {\n@@ -30,6 +32,11 @@ static int emit_response(const char *response, ...)\n \treturn 0;\n }\n \n+static int command_parse_error(const char *command)\n+{\n+\treturn emit_response(COMMAND_PARSE_ERROR, command, NULL);\n+}\n+\n /**\n  * A function pointer type for defining a command. The function is\n  * responsible for handling different versions of the command name.\n@@ -46,11 +53,248 @@ typedef int (*command_fn)(struct repository *repo,\n \t\t\t  char *data, size_t data_len);\n \n static int unknown_command(struct repository *repo UNUSED,\n-\t\t\t  char *data UNUSED, size_t data_len UNUSED)\n+\t\t\t   char *data UNUSED, size_t data_len UNUSED)\n {\n \treturn emit_response(UNKNOWN_COMMAND, NULL);\n }\n \n+static size_t parse_whitespace_token(char **data, size_t *data_len,\n+\t\t\t\t     char **token, int *err UNUSED)\n+{\n+\tsize_t i = 0;\n+\n+\t*token = *data;\n+\n+\twhile (i < *data_len && (*data)[i] && (*data)[i] != ' ')\n+\t\ti++;\n+\n+\tif (i >= *data_len) {\n+\t\t*data_len = 0;\n+\t\t*data = NULL;\n+\t\treturn i;\n+\t}\n+\n+\t(*data)[i] = 0;\n+\t*data_len = (*data_len) - (i + 1);\n+\t*data = *data + (i + 1);\n+\treturn i;\n+}\n+\n+/**\n+ * Given the remaining data line and its size, attempt to extract\n+ * a token. When the token delimiter is determined, the data\n+ * string is mutated to insert a NUL byte at the end of the token.\n+ * The data pointer is mutated to point at the next character (or\n+ * set to NULL if that exceeds the string length). The data_len\n+ * value is mutated to subtract the length of the discovered\n+ * token.\n+ *\n+ * The returned value is the length of the token that was\n+ * discovered.\n+ *\n+ * 'err' is ignored for now, but will be filled in in a future\n+ * change.\n+ */\n+static size_t parse_token(char **data, size_t *data_len,\n+\t\t\t  char **token, int *err)\n+{\n+\tif (!*data_len)\n+\t\treturn 0;\n+\n+\treturn parse_whitespace_token(data, data_len, token, err);\n+}\n+\n+enum value_match_mode {\n+\tMATCH_ALL,\n+\tMATCH_EXACT,\n+\tMATCH_REGEX,\n+};\n+\n+struct get_command_1_data {\n+\t/* parameters */\n+\tchar *key;\n+\tenum config_scope scope;\n+\tenum value_match_mode mode;\n+\n+\t/* optional parameters */\n+\tchar *value;\n+\tregex_t *value_pattern;\n+\n+\t/* data along the way, for single values. */\n+\tchar *found;\n+\tenum config_scope found_scope;\n+};\n+\n+static int get_command_1_cb(const char *key, const char *value,\n+\t\t\t    const struct config_context *context,\n+\t\t\t    void *data)\n+{\n+\tstruct get_command_1_data *d = data;\n+\n+\tif (strcasecmp(key, d->key))\n+\t\treturn 0;\n+\n+\tif (d->scope != CONFIG_SCOPE_UNKNOWN &&\n+\t    d->scope != context->kvi->scope)\n+\t\treturn 0;\n+\n+\tswitch (d->mode) {\n+\tcase MATCH_EXACT:\n+\t\tif (strcasecmp(value, d->value))\n+\t\t\treturn 0;\n+\t\tbreak;\n+\n+\tcase MATCH_REGEX:\n+\t\tif (regexec(d->value_pattern, value, 0, NULL, 0))\n+\t\t\treturn 0;\n+\t\tbreak;\n+\n+\tdefault:\n+\t\tbreak;\n+\t}\n+\n+\tfree(d->found);\n+\td->found = xstrdup(value);\n+\td->found_scope = context->kvi->scope;\n+\treturn 0;\n+}\n+\n+static const char *scope_str(enum config_scope scope)\n+{\n+\tswitch (scope) {\n+\tcase CONFIG_SCOPE_UNKNOWN:\n+\t\treturn \"unknown\";\n+\n+\tcase CONFIG_SCOPE_SYSTEM:\n+\t\treturn \"system\";\n+\n+\tcase CONFIG_SCOPE_GLOBAL:\n+\t\treturn \"global\";\n+\n+\tcase CONFIG_SCOPE_LOCAL:\n+\t\treturn \"local\";\n+\n+\tcase CONFIG_SCOPE_WORKTREE:\n+\t\treturn \"worktree\";\n+\n+\tcase CONFIG_SCOPE_SUBMODULE:\n+\t\treturn \"submodule\";\n+\n+\tcase CONFIG_SCOPE_COMMAND:\n+\t\treturn \"command\";\n+\n+\tdefault:\n+\t\tBUG(\"invalid config scope\");\n+\t}\n+}\n+\n+static int parse_scope(const char *str, enum config_scope *scope)\n+{\n+\tif (!strcmp(str, \"inherited\")) {\n+\t\t*scope = CONFIG_SCOPE_UNKNOWN;\n+\t\treturn 0;\n+\t}\n+\n+\tfor (enum config_scope s = 0; s < CONFIG_SCOPE__NR; s++) {\n+\t\tif (!strcmp(str, scope_str(s))) {\n+\t\t\t*scope = s;\n+\t\t\treturn 0;\n+\t\t}\n+\t}\n+\n+\treturn -1;\n+}\n+\n+/**\n+ * 'get' command, version 1.\n+ *\n+ * Positional arguments should be of the form:\n+ *\n+ * [0] scope (\"system\", \"global\", \"local\", \"worktree\", \"command\", \"submodule\", or \"inherited\")\n+ * [1] config key\n+ * [2*] multi-mode (\"regex\", \"fixed-value\")\n+ * [3*] value regex OR value string\n+ *\n+ * [N*] indicates optional parameters that are not needed.\n+ */\n+static int get_command_1(struct repository *repo,\n+\t\t\t char *data,\n+\t\t\t size_t data_len)\n+{\n+\tstruct get_command_1_data gc_data = {\n+\t\t.found = NULL,\n+\t\t.mode = MATCH_ALL,\n+\t};\n+\tint res = 0, err = 0;\n+\tchar *token;\n+\tsize_t token_len;\n+\n+\tif (!parse_token(&data, &data_len, &token, &err) || err)\n+\t\tgoto parse_error;\n+\n+\tif (parse_scope(token, &gc_data.scope))\n+\t\tgoto parse_error;\n+\n+\tif (!parse_token(&data, &data_len, &gc_data.key, &err) || err)\n+\t\tgoto parse_error;\n+\n+\ttoken_len = parse_token(&data, &data_len, &token, &err);\n+\tif (err)\n+\t\tgoto parse_error;\n+\n+\tif (token_len && !strncmp(token, \"arg:\", 4)) {\n+\t\tif (!strcmp(token + 4, \"regex\"))\n+\t\t\tgc_data.mode = MATCH_REGEX;\n+\t\telse if (!strcmp(token + 4, \"fixed-value\"))\n+\t\t\tgc_data.mode = MATCH_EXACT;\n+\t\telse\n+\t\t\tgoto parse_error; /* unknown arg. */\n+\n+\t\t/* Use the remaining data as the value string. */\n+\t\tgc_data.value = data;\n+\n+\t\tif (gc_data.mode == MATCH_REGEX) {\n+\t\t\tCALLOC_ARRAY(gc_data.value_pattern, 1);\n+\t\t\tif (regcomp(gc_data.value_pattern, gc_data.value,\n+\t\t\t\t    REG_EXTENDED)) {\n+\t\t\t\tFREE_AND_NULL(gc_data.value_pattern);\n+\t\t\t\tgoto parse_error;\n+\t\t\t}\n+\t\t}\n+\t} else if (token_len) {\n+\t\t/*\n+\t\t * If we have remaining tokens not starting in \"arg:\",\n+\t\t * then we don't understand them.\n+\t\t */\n+\t\tgoto parse_error;\n+\t}\n+\n+\trepo_config(repo, get_command_1_cb, &gc_data);\n+\n+\tif (gc_data.found)\n+\t\tres = emit_response(GET_COMMAND, \"1\", \"found\", gc_data.key,\n+\t\t\t\t    scope_str(gc_data.found_scope),\n+\t\t\t\t    gc_data.found,\n+\t\t\t\t    NULL);\n+\telse\n+\t\tres = emit_response(GET_COMMAND, \"1\", \"missing\", gc_data.key,\n+\t\t\t\t    gc_data.value, NULL);\n+\n+\tgoto cleanup;\n+\n+\n+parse_error:\n+\tres = command_parse_error(GET_COMMAND);\n+\n+cleanup:\n+\tif (gc_data.value_pattern) {\n+\t\tregfree(gc_data.value_pattern);\n+\t\tfree(gc_data.value_pattern);\n+\t}\n+\tfree(gc_data.found);\n+\treturn res;\n+}\n+\n struct command {\n \tconst char *name;\n \tcommand_fn fn;\n@@ -58,6 +302,11 @@ struct command {\n };\n \n static struct command commands[] = {\n+\t{\n+\t\t.name = GET_COMMAND,\n+\t\t.fn = get_command_1,\n+\t\t.version = 1,\n+\t},\n \t/* unknown_command must be last. */\n \t{\n \t\t.name = \"\",\ndiff --git a/config.h b/config.h\nindex ba426a960a..966a228f0e 100644\n--- a/config.h\n+++ b/config.h\n@@ -44,6 +44,9 @@ enum config_scope {\n \tCONFIG_SCOPE_WORKTREE,\n \tCONFIG_SCOPE_COMMAND,\n \tCONFIG_SCOPE_SUBMODULE,\n+\n+\t/* Must be last */\n+\tCONFIG_SCOPE__NR\n };\n const char *config_scope_name(enum config_scope scope);\n \ndiff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh\nindex f60ef35e38..e638b54d13 100755\n--- a/t/t1312-config-batch.sh\n+++ b/t/t1312-config-batch.sh\n@@ -16,10 +16,111 @@ test_expect_success 'unknown_command' '\n \ttest_cmp expect out\n '\n \n+test_expect_success 'completely broken input' '\n+\techo \"not_even_two_tokens\" >in &&\n+\ttest_must_fail git config-batch 2>err <in &&\n+\ttest_grep \"expected at least 2 tokens\" err &&\n+\ttest_grep \"an unrecoverable error occurred during command execution\" err\n+'\n+\n test_expect_success 'failed to parse version' '\n \techo \"bogus BAD_VERSION line of tokens\" >in &&\n \ttest_must_fail git config-batch 2>err <in &&\n \ttest_grep BAD_VERSION err\n '\n \n+test_expect_success 'get inherited config' '\n+\ttest_when_finished git config --unset test.key &&\n+\n+\tgit config test.key \"test value with spaces\" &&\n+\n+\techo \"get 1 inherited test.key\" >in &&\n+\techo \"get 1 found test.key local test value with spaces\" >expect &&\n+\tgit config-batch >out <in &&\n+\ttest_cmp expect out &&\n+\n+\techo \"get 1 global test.key\" >in &&\n+\techo \"get 1 missing test.key\" >expect &&\n+\tgit config-batch >out <in &&\n+\ttest_cmp expect out\n+'\n+\n+test_expect_success 'set up worktree' '\n+\ttest_commit A &&\n+\tgit config extensions.worktreeconfig true &&\n+\tgit worktree add --detach worktree\n+'\n+\n+test_expect_success 'get config with arg:regex' '\n+\ttest_when_finished git config --unset-all test.key &&\n+\tGIT_CONFIG_SYSTEM=system-config-file &&\n+\tGIT_CONFIG_NOSYSTEM=0 &&\n+\tGIT_CONFIG_GLOBAL=global-config-file &&\n+\texport GIT_CONFIG_SYSTEM &&\n+\texport GIT_CONFIG_NOSYSTEM &&\n+\texport GIT_CONFIG_GLOBAL &&\n+\n+\tgit config --system test.key on1e &&\n+\tgit config --global test.key t2wo &&\n+\tgit config test.key \"thre3e space\" &&\n+\tgit config --worktree test.key 4four &&\n+\n+\tcat >in <<-\\EOF &&\n+\tget 1 inherited test.key arg:regex .*1.*\n+\tget 1 inherited test.key arg:regex [a-z]2.*\n+\tget 1 inherited test.key arg:regex .*3e s.*\n+\tget 1 inherited test.key arg:regex 4.*\n+\tget 1 inherited test.key arg:regex .*5.*\n+\tget 1 inherited test.key arg:regex .*6.*\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tget 1 found test.key system on1e\n+\tget 1 found test.key global t2wo\n+\tget 1 found test.key local thre3e space\n+\tget 1 found test.key worktree 4four\n+\tget 1 found test.key command five5\n+\tget 1 missing test.key .*6.*\n+\tEOF\n+\n+\tgit -c test.key=five5 config-batch >out <in &&\n+\ttest_cmp expect out\n+'\n+\n+test_expect_success 'get config with arg:fixed-value' '\n+\ttest_when_finished git config --unset-all test.key &&\n+\tGIT_CONFIG_SYSTEM=system-config-file &&\n+\tGIT_CONFIG_NOSYSTEM=0 &&\n+\tGIT_CONFIG_GLOBAL=global-config-file &&\n+\texport GIT_CONFIG_SYSTEM &&\n+\texport GIT_CONFIG_NOSYSTEM &&\n+\texport GIT_CONFIG_GLOBAL &&\n+\n+\tgit config --system test.key one &&\n+\tgit config --global test.key two &&\n+\tgit config test.key \"three space\" &&\n+\tgit config --worktree test.key four &&\n+\n+\tcat >in <<-\\EOF &&\n+\tget 1 inherited test.key arg:fixed-value one\n+\tget 1 inherited test.key arg:fixed-value two\n+\tget 1 inherited test.key arg:fixed-value three space\n+\tget 1 inherited test.key arg:fixed-value four\n+\tget 1 inherited test.key arg:fixed-value five\n+\tget 1 inherited test.key arg:fixed-value six\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tget 1 found test.key system one\n+\tget 1 found test.key global two\n+\tget 1 found test.key local three space\n+\tget 1 found test.key worktree four\n+\tget 1 found test.key command five\n+\tget 1 missing test.key six\n+\tEOF\n+\n+\tgit -c test.key=five config-batch >out <in &&\n+\ttest_cmp expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"535153","messageId":"d5e0c32497581e6ac4890c6e71c5c33b92d67d51.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 04/11] config-batch: create 'help' command","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:19:56Z","receivedAt":"2026-02-04T14:20:14Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nTools that use the 'git config-batch' tool will want to know which commands\nare available in the current Git version. Having a 'help' command assists\ngreatly to give a clear set of available commands and their versions.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-config-batch.adoc | 17 +++++++++++++++\n builtin/config-batch.c              | 32 +++++++++++++++++++++++++++++\n t/t1312-config-batch.sh             | 13 ++++++++++++\n 3 files changed, 62 insertions(+)\n\ndiff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\nindex 31dd42f481..1fff68a13c 100644\n--- a/Documentation/git-config-batch.adoc\n+++ b/Documentation/git-config-batch.adoc\n@@ -38,6 +38,23 @@ unknown_command LF\n \n These are the commands that are currently understood:\n \n+`help` version 1::\n+\tThe `help` command lists the currently-available commands in\n+\tthis version of Git. The output is multi-line, but the first\n+\tline provides the count of possible commands via `help count <N>`.\n+\tThe next `<N>` lines are of the form `help <command> <version>`\n+\tto state that this Git version supports that `<command>` at\n+\tversion `<version>`. Note that the same command may have multiple\n+\tavailable versions.\n++\n+Here is the currentl output of the help text at the latest version:\n++\n+------------\n+help 1 count 2\n+help 1 help 1\n+help 1 get 1\n+------------\n+\n `get` version 1::\n \tThe `get` command searches the config key-value pairs within a\n \tgiven `<scope>` for values that match the fixed `<key>` and\ndiff --git a/builtin/config-batch.c b/builtin/config-batch.c\nindex 5782004080..1c19e4889f 100644\n--- a/builtin/config-batch.c\n+++ b/builtin/config-batch.c\n@@ -12,6 +12,7 @@ static const char *const builtin_config_batch_usage[] = {\n };\n \n #define UNKNOWN_COMMAND \"unknown_command\"\n+#define HELP_COMMAND \"help\"\n #define GET_COMMAND \"get\"\n #define COMMAND_PARSE_ERROR \"command_parse_error\"\n \n@@ -104,6 +105,9 @@ static size_t parse_token(char **data, size_t *data_len,\n \treturn parse_whitespace_token(data, data_len, token, err);\n }\n \n+static int help_command_1(struct repository *repo,\n+\t\t\t  char *data, size_t data_len);\n+\n enum value_match_mode {\n \tMATCH_ALL,\n \tMATCH_EXACT,\n@@ -302,6 +306,11 @@ struct command {\n };\n \n static struct command commands[] = {\n+\t{\n+\t\t.name = HELP_COMMAND,\n+\t\t.fn = help_command_1,\n+\t\t.version = 1,\n+\t},\n \t{\n \t\t.name = GET_COMMAND,\n \t\t.fn = get_command_1,\n@@ -316,6 +325,29 @@ static struct command commands[] = {\n \n #define COMMAND_COUNT ((size_t)(sizeof(commands) / sizeof(*commands)))\n \n+static int help_command_1(struct repository *repo UNUSED,\n+\t\t\t  char *data UNUSED, size_t data_len UNUSED)\n+{\n+\tstruct strbuf fmt_str = STRBUF_INIT;\n+\n+\tstrbuf_addf(&fmt_str, \"%\"PRIu32, (uint32_t)(COMMAND_COUNT - 1));\n+\temit_response(HELP_COMMAND, \"1\", \"count\", fmt_str.buf, NULL);\n+\tstrbuf_reset(&fmt_str);\n+\n+\tfor (size_t i = 0; i < COMMAND_COUNT; i++) {\n+\t\t/* Halt at unknown command. */\n+\t\tif (!commands[i].name[0])\n+\t\t\tbreak;\n+\n+\t\tstrbuf_addf(&fmt_str, \"%d\", commands[i].version);\n+\t\temit_response(HELP_COMMAND, \"1\", commands[i].name, fmt_str.buf, NULL);\n+\t\tstrbuf_reset(&fmt_str);\n+\t}\n+\n+\tstrbuf_release(&fmt_str);\n+\treturn 0;\n+}\n+\n /**\n  * Process a single line from stdin and process the command.\n  *\ndiff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh\nindex e638b54d13..6b550a0e76 100755\n--- a/t/t1312-config-batch.sh\n+++ b/t/t1312-config-batch.sh\n@@ -23,6 +23,19 @@ test_expect_success 'completely broken input' '\n \ttest_grep \"an unrecoverable error occurred during command execution\" err\n '\n \n+test_expect_success 'help command' '\n+\techo \"help 1\" >in &&\n+\n+\tcat >expect <<-\\EOF &&\n+\thelp 1 count 2\n+\thelp 1 help 1\n+\thelp 1 get 1\n+\tEOF\n+\n+\tgit config-batch >out <in &&\n+\ttest_cmp expect out\n+'\n+\n test_expect_success 'failed to parse version' '\n \techo \"bogus BAD_VERSION line of tokens\" >in &&\n \ttest_must_fail git config-batch 2>err <in &&\n-- \ngitgitgadget\n\n"},{"id":"535154","messageId":"33faa3f134c81761631c34600477dcbf82e619e5.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 05/11] config-batch: add NUL-terminated I/O format","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:19:57Z","receivedAt":"2026-02-04T14:20:15Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nWhen using automated tools, it is critical to allow for input/output formats\nthat include special characters such as spaces and newlines. While the\nexisting protocol for 'git config-batch' is human-readable and has some\ncapacity for some spaces in certain positions, it is not available for\nspaces in the config key or newlines in the config values.\n\nAdd the '-z' option to signal the use of NUL-terminated strings. To\nunderstand where commands end regardless of potential future formats, use\ntwo NUL bytes in a row to terminate a command. To allow for empty string\nvalues, each token is provided in a <length>:<value> format, making \"0:\"\nthe empty string value.\n\nUpdate the existing 'help' and 'get' commands to match this format. Create\nhelper methods that make it easy to parse and print in both formats\nsimultaneously.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-config-batch.adoc |  57 ++++++++-\n builtin/config-batch.c              | 188 +++++++++++++++++++++++++---\n t/t1312-config-batch.sh             |  69 ++++++++++\n 3 files changed, 293 insertions(+), 21 deletions(-)\n\ndiff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\nindex 1fff68a13c..3c9a3bb763 100644\n--- a/Documentation/git-config-batch.adoc\n+++ b/Documentation/git-config-batch.adoc\n@@ -21,6 +21,15 @@ multiple configuration values, the `git config-batch` command allows a\n single process to handle multiple requests using a machine-parseable\n interface across `stdin` and `stdout`.\n \n+OPTIONS\n+-------\n+\n+`-z`::\n+\tIf specified, then use the NUL-terminated input and output\n+\tformat instead of the space and newline format. This format is\n+\tuseful when the strings involved may include spaces or newlines.\n+\tSee PROTOCOL for more details.\n+\n PROTOCOL\n --------\n By default, the protocol uses line feeds (`LF`) to signal the end of a\n@@ -41,13 +50,13 @@ These are the commands that are currently understood:\n `help` version 1::\n \tThe `help` command lists the currently-available commands in\n \tthis version of Git. The output is multi-line, but the first\n-\tline provides the count of possible commands via `help count <N>`.\n-\tThe next `<N>` lines are of the form `help <command> <version>`\n+\tline provides the count of possible commands via `help 1 count <N>`.\n+\tThe next `<N>` lines are of the form `help 1 <command> <version>`\n \tto state that this Git version supports that `<command>` at\n \tversion `<version>`. Note that the same command may have multiple\n \tavailable versions.\n +\n-Here is the currentl output of the help text at the latest version:\n+Here is the current output of the help text at the latest version:\n +\n ------------\n help 1 count 2\n@@ -102,6 +111,48 @@ get 1 missing <key> [<value-pattern>|<value>]\n where `<value-pattern>` or `<value>` is only supplied if provided in\n the command.\n \n+NUL-Terminated Format\n+~~~~~~~~~~~~~~~~~~~~~\n+\n+When `-z` is given, the protocol changes in some structural ways.\n+\n+First, each command is terminated with two NUL bytes, providing a clear\n+boundary between commands regardless of future possibilities of new\n+command formats.\n+\n+Second, any time that a space _would_ be used to partition tokens in a\n+command, a NUL byte is used instead. Further, each token is prefixed\n+with `<N>:` where `<N>` is a decimal representation of the length of\n+the string between the `:` and the next NUL byte. Any disagreement in\n+these lengths is treated as a parsing error. This use of a length does\n+imply that \"`0:`\" is the representation of an empty string, if relevant.\n+\n+The decimal representation must have at most five numerals, thus the\n+maximum length of a string token can have 99999 characters.\n+\n+For example, the `get` command, version 1, could have any of the\n+following forms:\n+\n+------------\n+3:get NUL 1:1 NUL 5:local NUL 14:key.with space NUL NUL\n+3:get NUL 1:1 NUL 9:inherit NUL 8:test.key NUL 9:arg:regex NUL 6:.*\\ .* NUL NUL\n+3:get NUL 1:1 NUL 6:global NUL 8:test.key NUL 15:arg:fixed-value NUL 3:a b NUL NUL\n+------------\n+\n+The output is modified similarly, such as the following output examples,\n+as if the input has a parse error, a valid `help` command, a `get`\n+command that had a match, and a `get` command that did not match.\n+\n+------------\n+15:unknown_command NUL NUL\n+4:help NUL 1:1 NUL 5:count NUL 1:2 NUL NUL\n+4:help NUL 1:1 NUL 4:help NUL 1:1 NUL NUL\n+4:help NUL 1:1 NUL 3:get NUL 1:1 NUL NUL\n+3:get NUL 1:1 NUL 5:found NUL 8:test.key NUL 5:value NUL NUL\n+3:get NUL 1:1 NUL 7:missing NUL 8:test.key NUL NUL\n+------------\n+\n+\n SEE ALSO\n --------\n linkgit:git-config[1]\ndiff --git a/builtin/config-batch.c b/builtin/config-batch.c\nindex 1c19e4889f..2c48c4ea37 100644\n--- a/builtin/config-batch.c\n+++ b/builtin/config-batch.c\n@@ -11,24 +11,40 @@ static const char *const builtin_config_batch_usage[] = {\n \tNULL\n };\n \n+static int zformat = 0;\n+\n #define UNKNOWN_COMMAND \"unknown_command\"\n #define HELP_COMMAND \"help\"\n #define GET_COMMAND \"get\"\n #define COMMAND_PARSE_ERROR \"command_parse_error\"\n \n+static void print_word(const char *word, int start)\n+{\n+\tif (zformat) {\n+\t\tprintf(\"%\"PRIu32\":%s\", (uint32_t)strlen(word), word);\n+\t\tfputc(0, stdout);\n+\t} else if (start)\n+\t\tprintf(\"%s\", word);\n+\telse\n+\t\tprintf(\" %s\", word);\n+}\n+\n static int emit_response(const char *response, ...)\n {\n \tva_list params;\n \tconst char *token;\n \n-\tprintf(\"%s\", response);\n+\tprint_word(response, 1);\n \n \tva_start(params, response);\n \twhile ((token = va_arg(params, const char *)))\n-\t\tprintf(\" %s\", token);\n+\t\tprint_word(token, 0);\n \tva_end(params);\n \n-\tprintf(\"\\n\");\n+\tif (zformat)\n+\t\tfputc(0, stdout);\n+\telse\n+\t\tprintf(\"\\n\");\n \tfflush(stdout);\n \treturn 0;\n }\n@@ -59,6 +75,52 @@ static int unknown_command(struct repository *repo UNUSED,\n \treturn emit_response(UNKNOWN_COMMAND, NULL);\n }\n \n+/*\n+ * Parse the next token using the NUL-byte format.\n+ */\n+static size_t parse_ztoken(char **data, size_t *data_len,\n+\t\t\t   char **token, int *err)\n+{\n+\tsize_t i = 0, token_len;\n+\n+\twhile (i < *data_len && (*data)[i] != ':') {\n+\t\tif ((*data)[i] < '0' || (*data)[i] > '9') {\n+\t\t\tgoto parse_error;\n+\t\t}\n+\t\ti++;\n+\t}\n+\n+\tif (i >= *data_len || (*data)[i] != ':' || i > 5)\n+\t\tgoto parse_error;\n+\n+\t(*data)[i] = 0;\n+\ttoken_len = atoi(*data);\n+\n+\tif (token_len + i + 1 >= *data_len)\n+\t\tgoto parse_error;\n+\n+\t*token = *data + i + 1;\n+\t*data_len = *data_len - (i + 1);\n+\n+\t/* check for early NULs. */\n+\tfor (i = 0; i < token_len; i++) {\n+\t\tif (!(*token)[i])\n+\t\t\tgoto parse_error;\n+\t}\n+\t/* check for matching NUL. */\n+\tif ((*token)[token_len])\n+\t\tgoto parse_error;\n+\n+\t*data = *token + token_len + 1;\n+\t*data_len = *data_len - (token_len + 1);\n+\treturn token_len;\n+\n+parse_error:\n+\t*err = 1;\n+\t*token = NULL;\n+\treturn 0;\n+}\n+\n static size_t parse_whitespace_token(char **data, size_t *data_len,\n \t\t\t\t     char **token, int *err UNUSED)\n {\n@@ -93,15 +155,23 @@ static size_t parse_whitespace_token(char **data, size_t *data_len,\n  * The returned value is the length of the token that was\n  * discovered.\n  *\n- * 'err' is ignored for now, but will be filled in in a future\n- * change.\n+ * The 'token' pointer is used to set the start of the token.\n+ * In the whitespace format, this is always the input value of\n+ * 'data' but in the NUL-terminated format this follows an \"<N>:\"\n+ * prefix.\n+ *\n+ * In the case of the NUL-terminated format, a bad parse of the\n+ * decimal length or a mismatch of the decimal length and the\n+ * length of the following NUL-terminated string will result in\n+ * the value pointed at by 'err' to be set to 1.\n  */\n static size_t parse_token(char **data, size_t *data_len,\n \t\t\t  char **token, int *err)\n {\n \tif (!*data_len)\n \t\treturn 0;\n-\n+\tif (zformat)\n+\t\treturn parse_ztoken(data, data_len, token, err);\n \treturn parse_whitespace_token(data, data_len, token, err);\n }\n \n@@ -255,7 +325,13 @@ static int get_command_1(struct repository *repo,\n \t\t\tgoto parse_error; /* unknown arg. */\n \n \t\t/* Use the remaining data as the value string. */\n-\t\tgc_data.value = data;\n+\t\tif (!zformat)\n+\t\t\tgc_data.value = data;\n+\t\telse {\n+\t\t\tparse_token(&data, &data_len, &gc_data.value, &err);\n+\t\t\tif (err)\n+\t\t\t\tgoto parse_error;\n+\t\t}\n \n \t\tif (gc_data.mode == MATCH_REGEX) {\n \t\t\tCALLOC_ARRAY(gc_data.value_pattern, 1);\n@@ -348,17 +424,74 @@ static int help_command_1(struct repository *repo UNUSED,\n \treturn 0;\n }\n \n-/**\n- * Process a single line from stdin and process the command.\n- *\n- * Returns 0 on successful processing of command, including the\n- * unknown_command output.\n- *\n- * Returns 1 on natural exit due to exist signal of empty line.\n- *\n- * Returns negative value on other catastrophic error.\n- */\n-static int process_command(struct repository *repo)\n+static int process_command_nul(struct repository *repo)\n+{\n+\tstatic struct strbuf line = STRBUF_INIT;\n+\tchar *data, *command, *versionstr;\n+\tsize_t data_len, token_len;\n+\tint res = 0, err = 0, version = 0, getc;\n+\tchar c;\n+\n+\t/* If we start with EOF it's not an error. */\n+\tgetc = fgetc(stdin);\n+\tif (getc == EOF)\n+\t\treturn 1;\n+\n+\tdo {\n+\t\tc = (char)getc;\n+\t\tstrbuf_addch(&line, c);\n+\n+\t\tif (!c && line.len > 1 && !line.buf[line.len - 2])\n+\t\t\tbreak;\n+\n+\t\tgetc = fgetc(stdin);\n+\n+\t\t/* It's an error if we reach EOF while parsing a command. */\n+\t\tif (getc == EOF)\n+\t\t\tgoto parse_error;\n+\t} while (1);\n+\n+\tdata = line.buf;\n+\tdata_len = line.len - 1;\n+\n+\ttoken_len = parse_ztoken(&data, &data_len, &command, &err);\n+\tif (!token_len || err)\n+\t\tgoto parse_error;\n+\n+\ttoken_len = parse_ztoken(&data, &data_len, &versionstr, &err);\n+\tif (!token_len || err)\n+\t\tgoto parse_error;\n+\n+\tif (!git_parse_int(versionstr, &version)) {\n+\t\tres = error(_(\"unable to parse '%s' to integer\"),\n+\t\t\t    versionstr);\n+\t\tgoto parse_error;\n+\t}\n+\n+\tfor (size_t i = 0; i < COMMAND_COUNT; i++) {\n+\t\t/*\n+\t\t * Run the ith command if we have hit the unknown\n+\t\t * command or if the name and version match.\n+\t\t */\n+\t\tif (!commands[i].name[0] ||\n+\t\t    (!strcmp(command, commands[i].name) &&\n+\t\t     commands[i].version == version)) {\n+\t\t\tres = commands[i].fn(repo, data, data_len);\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t}\n+\n+\tBUG(_(\"scanned to end of command list, including 'unknown_command'\"));\n+\n+parse_error:\n+\tres = unknown_command(repo, NULL, 0);\n+\n+cleanup:\n+\tstrbuf_release(&line);\n+\treturn res;\n+}\n+\n+static int process_command_whitespace(struct repository *repo)\n {\n \tstatic struct strbuf line = STRBUF_INIT;\n \tstruct string_list tokens = STRING_LIST_INIT_NODUP;\n@@ -416,6 +549,23 @@ cleanup:\n \treturn res;\n }\n \n+/**\n+ * Process a single line from stdin and process the command.\n+ *\n+ * Returns 0 on successful processing of command, including the\n+ * unknown_command output.\n+ *\n+ * Returns 1 on natural exit due to exist signal of empty line.\n+ *\n+ * Returns negative value on other catastrophic error.\n+ */\n+static int process_command(struct repository *repo)\n+{\n+\tif (zformat)\n+\t\treturn process_command_nul(repo);\n+\treturn process_command_whitespace(repo);\n+}\n+\n int cmd_config_batch(int argc,\n \t\t     const char **argv,\n \t\t     const char *prefix,\n@@ -423,6 +573,8 @@ int cmd_config_batch(int argc,\n {\n \tint res = 0;\n \tstruct option options[] = {\n+\t\tOPT_BOOL('z', NULL, &zformat,\n+\t\t\t N_(\"stdin and stdout is NUL-terminated\")),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh\nindex 6b550a0e76..f7a74ddc2c 100755\n--- a/t/t1312-config-batch.sh\n+++ b/t/t1312-config-batch.sh\n@@ -4,6 +4,26 @@ test_description='Test git config-batch'\n \n . ./test-lib.sh\n \n+# usage: test_zformat <command> <args> <in >out\n+#\n+# Let 'in' be a z-format input but with \" NUL \" between tokens in\n+# a single command and \" NUL NUL\" trailing each line.\n+#\n+# The values in 'out' will be space- and newline-delimited where\n+# NUL-bytes would normally be output.\n+test_zformat () {\n+\tsed -e \"s/\\ NUL\\ /!/g\" >nullin1 &&\n+\tsed -e \"s/NUL//g\" <nullin1 >nullin2 &&\n+\n+\ttr \"!\" \"\\0\" <nullin2 >nullin3 &&\n+\ttr \"\\n\" \"\\0\" <nullin3 >zin &&\n+\n+\t$* <zin >zout &&\n+\n+\ttr \"\\0\" \" \" <zout >outspace &&\n+\tsed \"s/\\ \\ /\\n/g\" <outspace\n+}\n+\n test_expect_success 'no commands' '\n \techo | git config-batch >out &&\n \ttest_must_be_empty out\n@@ -36,6 +56,23 @@ test_expect_success 'help command' '\n \ttest_cmp expect out\n '\n \n+test_expect_success 'help -z' '\n+\tcat >in <<-\\EOF &&\n+\t4:help NUL 1:1 NUL NUL\n+\t5:bogus NUL 2:10 NUL NUL\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\t4:help 1:1 5:count 1:2\n+\t4:help 1:1 4:help 1:1\n+\t4:help 1:1 3:get 1:1\n+\t15:unknown_command\n+\tEOF\n+\n+\ttest_zformat git config-batch -z >out <in &&\n+\ttest_cmp expect out\n+'\n+\n test_expect_success 'failed to parse version' '\n \techo \"bogus BAD_VERSION line of tokens\" >in &&\n \ttest_must_fail git config-batch 2>err <in &&\n@@ -136,4 +173,36 @@ test_expect_success 'get config with arg:fixed-value' '\n \ttest_cmp expect out\n '\n \n+test_expect_success 'get config with -z' '\n+\ttest_when_finished git config --unset-all test.key &&\n+\tGIT_CONFIG_SYSTEM=system-config-file &&\n+\tGIT_CONFIG_NOSYSTEM=0 &&\n+\tGIT_CONFIG_GLOBAL=global-config-file &&\n+\texport GIT_CONFIG_SYSTEM &&\n+\texport GIT_CONFIG_NOSYSTEM &&\n+\texport GIT_CONFIG_GLOBAL &&\n+\n+\tgit config --system test.key on1e &&\n+\tgit config --global test.key t2wo &&\n+\tgit config test.key \"thre3e space\" &&\n+\tgit config --worktree test.key 4four &&\n+\n+\tcat >in <<-\\EOF &&\n+\t3:get NUL 1:1 NUL 9:inherited NUL 8:test.key NUL NUL\n+\t3:get NUL 1:1 NUL 6:global NUL 8:test.key NUL 9:arg:regex NUL 3:2.* NUL NUL\n+\t3:get NUL 1:1 NUL 5:local NUL 8:test.key NUL 15:arg:fixed-value NUL 12:thre3e space NUL NUL\n+\t3:get NUL 1:1 NUL 9:inherited NUL 11:key.missing NUL NUL\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\t3:get 1:1 5:found 8:test.key 8:worktree 5:4four\n+\t3:get 1:1 5:found 8:test.key 6:global 4:t2wo\n+\t3:get 1:1 5:found 8:test.key 5:local 12:thre3e space\n+\t3:get 1:1 7:missing 11:key.missing\n+\tEOF\n+\n+\ttest_zformat git config-batch -z >out <in &&\n+\ttest_cmp expect out\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"535155","messageId":"014e959cf4a4e19afe6becdb155f49d0f96739f8.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 06/11] docs: add design doc for config-batch","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:19:58Z","receivedAt":"2026-02-04T14:20:16Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThis document will be a place that tracks the future directions of the\n'git config-batch' builtin. We plan to remove items as they are\nimplemented in new commands and documented in the builtin documentation.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/technical/config-batch.adoc | 70 +++++++++++++++++++++++\n 1 file changed, 70 insertions(+)\n create mode 100644 Documentation/technical/config-batch.adoc\n\ndiff --git a/Documentation/technical/config-batch.adoc b/Documentation/technical/config-batch.adoc\nnew file mode 100644\nindex 0000000000..dbd614ad4f\n--- /dev/null\n+++ b/Documentation/technical/config-batch.adoc\n@@ -0,0 +1,70 @@\n+Git Config-Batch Design Notes\n+=============================\n+\n+The `git config-batch` builtin has a robust protocol for parsing multiple\n+commands over `stdin` and providing structured output over `stdout`. The\n+intended use is for scripts or third-party software to interact with the\n+config settings of a repository multiple times within the same Git process.\n+The protocol is built with versioning that allows the consumer to know when\n+a certain command is available and to fall back to single-use `git config`\n+processes if the installed Git version does not have the latest commands\n+at the required versions.\n+\n+Recommended interaction pattern\n+-------------------------------\n+\n+This section provides a guide for ideal interaction with the `git\n+config-batch` command and its protocol.\n+\n+For maximum compatibility, do not attempt parsing the output of `git\n+version` to determine which commands are available. Instead, first check\n+if the `git config-batch` command succeeds and does not die immediately\n+due to the builtin being unavailable. Then, use the v1 of the `help`\n+command to get a list of available commands and versions. Use this list to\n+determine if your capabilities are available or should be replaced with an\n+appropriate `git config` single-use process.\n+\n+Further, all automated tooling would be better off using the\n+NUL-terminated format instead of the whitespace-delimited format, in case\n+config keys contain spaces or config values contain newlines. The\n+whitespace-delimited version is available for simpler integration and\n+human inspection.\n+\n+Current commands\n+----------------\n+\n+See the documentation in linkgit::config-batch[1] for the latest set of\n+available commands and their protocols.\n+\n+Future commands\n+---------------\n+\n+The following modes of `git config` are not currently available as commands\n+in `git config-batch`, but are planned for future integration:\n+\n+`git config list [--<scope>]`::\n+\tGetting all values, regardless of config key, would require a\n+\tmulti-valued output similar to the `help` command. This tool will\n+\tlikely assume advanced options such as `--show-origin`.\n+\n+`git config set [--<scope>] <key> <value>`::\n+\tIt will be desirable to set a config key at a given scope as a\n+\tsingle value, replacing the current value at that scope, if it\n+\texists and is a single value. A `set` command could satisfy this\n+\tpurpose.\n+\n+`git config set --all [<value-pattern>|--fixed-value=<fixedvalue>] <key> <value>`::\n+\tWhen replacing multiple values, it may be necessary to have a different\n+\toutput describing the places those values were set, so it may need to\n+\tbe implemented via a `set-all` command to differentiate from a `set`\n+\tcommand.\n+\n+`git config unset <key>`::\n+\n+`git config unset --all [<value-pattern>|--fixed-value=<fixedvalue>] <key>`::\n+\n+`git config get --all --rexexp <key-pattern> [<value-options>]`::\n+\n+`--replace-all` option::\n+\n+`--type=<type>` option::\n-- \ngitgitgadget\n\n"},{"id":"535156","messageId":"4be089a4dda63fdc0ea2db00acb47b33befe07ef.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 07/11] config: extract location structs from builtin","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:19:59Z","receivedAt":"2026-02-04T14:20:18Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nBefore reusing these concepts in builtin/config-batch.c, extract the\nconfig_location_options struct from builtin/config.c to config.h with\nimplementation in config.c.\n\nThe only modification in this conversion is the use of a repository\nparameter instead of the_repository.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/config.c | 117 ++++-------------------------------------------\n config.c         |  89 +++++++++++++++++++++++++++++++++++\n config.h         |  20 ++++++++\n 3 files changed, 117 insertions(+), 109 deletions(-)\n\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 288ebdfdaa..d129b1204d 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -71,20 +71,6 @@ static const char *const builtin_config_edit_usage[] = {\n \tOPT_STRING('f', \"file\", &opts.source.file, N_(\"file\"), N_(\"use given config file\")), \\\n \tOPT_STRING(0, \"blob\", &opts.source.blob, N_(\"blob-id\"), N_(\"read config from given blob object\"))\n \n-struct config_location_options {\n-\tstruct git_config_source source;\n-\tstruct config_options options;\n-\tchar *file_to_free;\n-\tint use_global_config;\n-\tint use_system_config;\n-\tint use_local_config;\n-\tint use_worktree_config;\n-\tint respect_includes_opt;\n-};\n-#define CONFIG_LOCATION_OPTIONS_INIT { \\\n-\t.respect_includes_opt = -1, \\\n-}\n-\n #define CONFIG_TYPE_OPTIONS(type) \\\n \tOPT_GROUP(N_(\"Type\")), \\\n \tOPT_CALLBACK('t', \"type\", &type, N_(\"type\"), N_(\"value is given this type\"), option_parse_type), \\\n@@ -772,93 +758,6 @@ static char *default_user_config(void)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static void location_options_init(struct config_location_options *opts,\n-\t\t\t\t  const char *prefix)\n-{\n-\tif (!opts->source.file)\n-\t\topts->source.file = opts->file_to_free =\n-\t\t\txstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n-\n-\tif (opts->use_global_config + opts->use_system_config +\n-\t    opts->use_local_config + opts->use_worktree_config +\n-\t    !!opts->source.file + !!opts->source.blob > 1) {\n-\t\terror(_(\"only one config file at a time\"));\n-\t\texit(129);\n-\t}\n-\n-\tif (!startup_info->have_repository) {\n-\t\tif (opts->use_local_config)\n-\t\t\tdie(_(\"--local can only be used inside a git repository\"));\n-\t\tif (opts->source.blob)\n-\t\t\tdie(_(\"--blob can only be used inside a git repository\"));\n-\t\tif (opts->use_worktree_config)\n-\t\t\tdie(_(\"--worktree can only be used inside a git repository\"));\n-\t}\n-\n-\tif (opts->source.file &&\n-\t\t\t!strcmp(opts->source.file, \"-\")) {\n-\t\topts->source.file = NULL;\n-\t\topts->source.use_stdin = 1;\n-\t\topts->source.scope = CONFIG_SCOPE_COMMAND;\n-\t}\n-\n-\tif (opts->use_global_config) {\n-\t\topts->source.file = opts->file_to_free = git_global_config();\n-\t\tif (!opts->source.file)\n-\t\t\t/*\n-\t\t\t * It is unknown if HOME/.gitconfig exists, so\n-\t\t\t * we do not know if we should write to XDG\n-\t\t\t * location; error out even if XDG_CONFIG_HOME\n-\t\t\t * is set and points at a sane location.\n-\t\t\t */\n-\t\t\tdie(_(\"$HOME not set\"));\n-\t\topts->source.scope = CONFIG_SCOPE_GLOBAL;\n-\t} else if (opts->use_system_config) {\n-\t\topts->source.file = opts->file_to_free = git_system_config();\n-\t\topts->source.scope = CONFIG_SCOPE_SYSTEM;\n-\t} else if (opts->use_local_config) {\n-\t\topts->source.file = opts->file_to_free = repo_git_path(the_repository, \"config\");\n-\t\topts->source.scope = CONFIG_SCOPE_LOCAL;\n-\t} else if (opts->use_worktree_config) {\n-\t\tstruct worktree **worktrees = get_worktrees();\n-\t\tif (the_repository->repository_format_worktree_config)\n-\t\t\topts->source.file = opts->file_to_free =\n-\t\t\t\trepo_git_path(the_repository, \"config.worktree\");\n-\t\telse if (worktrees[0] && worktrees[1])\n-\t\t\tdie(_(\"--worktree cannot be used with multiple \"\n-\t\t\t      \"working trees unless the config\\n\"\n-\t\t\t      \"extension worktreeConfig is enabled. \"\n-\t\t\t      \"Please read \\\"CONFIGURATION FILE\\\"\\n\"\n-\t\t\t      \"section in \\\"git help worktree\\\" for details\"));\n-\t\telse\n-\t\t\topts->source.file = opts->file_to_free =\n-\t\t\t\trepo_git_path(the_repository, \"config\");\n-\t\topts->source.scope = CONFIG_SCOPE_LOCAL;\n-\t\tfree_worktrees(worktrees);\n-\t} else if (opts->source.file) {\n-\t\tif (!is_absolute_path(opts->source.file) && prefix)\n-\t\t\topts->source.file = opts->file_to_free =\n-\t\t\t\tprefix_filename(prefix, opts->source.file);\n-\t\topts->source.scope = CONFIG_SCOPE_COMMAND;\n-\t} else if (opts->source.blob) {\n-\t\topts->source.scope = CONFIG_SCOPE_COMMAND;\n-\t}\n-\n-\tif (opts->respect_includes_opt == -1)\n-\t\topts->options.respect_includes = !opts->source.file;\n-\telse\n-\t\topts->options.respect_includes = opts->respect_includes_opt;\n-\tif (startup_info->have_repository) {\n-\t\topts->options.commondir = repo_get_common_dir(the_repository);\n-\t\topts->options.git_dir = repo_get_git_dir(the_repository);\n-\t}\n-}\n-\n-static void location_options_release(struct config_location_options *opts)\n-{\n-\tfree(opts->file_to_free);\n-}\n-\n static void display_options_init(struct config_display_options *opts)\n {\n \tif (opts->end_nul) {\n@@ -885,7 +784,7 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix,\n \targc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0);\n \tcheck_argc(argc, 0, 0);\n \n-\tlocation_options_init(&location_opts, prefix);\n+\tlocation_options_init(the_repository, &location_opts, prefix);\n \tdisplay_options_init(&display_opts);\n \n \tsetup_auto_pager(\"config\", 1);\n@@ -944,7 +843,7 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix,\n \t\t    value_pattern))\n \t\tdie(_(\"--url= cannot be used with --all, --regexp or --value\"));\n \n-\tlocation_options_init(&location_opts, prefix);\n+\tlocation_options_init(the_repository, &location_opts, prefix);\n \tdisplay_options_init(&display_opts);\n \n \tif (display_opts.type != TYPE_COLOR)\n@@ -998,7 +897,7 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix,\n \n \tcomment = git_config_prepare_comment_string(comment_arg);\n \n-\tlocation_options_init(&location_opts, prefix);\n+\tlocation_options_init(the_repository, &location_opts, prefix);\n \tcheck_write(&location_opts.source);\n \n \tvalue = normalize_value(argv[0], argv[1], type, &default_kvi);\n@@ -1044,7 +943,7 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix,\n \tif ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern)\n \t\tdie(_(\"--fixed-value only applies with 'value-pattern'\"));\n \n-\tlocation_options_init(&location_opts, prefix);\n+\tlocation_options_init(the_repository, &location_opts, prefix);\n \tcheck_write(&location_opts.source);\n \n \tif ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern)\n@@ -1073,7 +972,7 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \tcheck_argc(argc, 2, 2);\n \n-\tlocation_options_init(&location_opts, prefix);\n+\tlocation_options_init(the_repository, &location_opts, prefix);\n \tcheck_write(&location_opts.source);\n \n \tret = repo_config_rename_section_in_file(the_repository, location_opts.source.file,\n@@ -1103,7 +1002,7 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \tcheck_argc(argc, 1, 1);\n \n-\tlocation_options_init(&location_opts, prefix);\n+\tlocation_options_init(the_repository, &location_opts, prefix);\n \tcheck_write(&location_opts.source);\n \n \tret = repo_config_rename_section_in_file(the_repository, location_opts.source.file,\n@@ -1163,7 +1062,7 @@ static int cmd_config_edit(int argc, const char **argv, const char *prefix,\n \targc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0);\n \tcheck_argc(argc, 0, 0);\n \n-\tlocation_options_init(&location_opts, prefix);\n+\tlocation_options_init(the_repository, &location_opts, prefix);\n \tcheck_write(&location_opts.source);\n \n \tret = show_editor(&location_opts);\n@@ -1231,7 +1130,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix)\n \t\t\t     builtin_config_usage,\n \t\t\t     PARSE_OPT_STOP_AT_NON_OPTION);\n \n-\tlocation_options_init(&location_opts, prefix);\n+\tlocation_options_init(the_repository, &location_opts, prefix);\n \tdisplay_options_init(&display_opts);\n \n \tif ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && display_opts.type) {\ndiff --git a/config.c b/config.c\nindex 7f6d53b473..9f1a7b45cf 100644\n--- a/config.c\n+++ b/config.c\n@@ -35,6 +35,7 @@\n #include \"strvec.h\"\n #include \"trace2.h\"\n #include \"wildmatch.h\"\n+#include \"worktree.h\"\n #include \"write-or-die.h\"\n \n struct config_source {\n@@ -3592,3 +3593,91 @@ int lookup_config(const char **mapping, int nr_mapping, const char *var)\n \t}\n \treturn -1;\n }\n+\n+void location_options_init(struct repository *repo,\n+\t\t\t   struct config_location_options *opts,\n+\t\t\t   const char *prefix)\n+{\n+\tif (!opts->source.file)\n+\t\topts->source.file = opts->file_to_free =\n+\t\t\txstrdup_or_null(getenv(CONFIG_ENVIRONMENT));\n+\n+\tif (opts->use_global_config + opts->use_system_config +\n+\t    opts->use_local_config + opts->use_worktree_config +\n+\t    !!opts->source.file + !!opts->source.blob > 1) {\n+\t\terror(_(\"only one config file at a time\"));\n+\t\texit(129);\n+\t}\n+\n+\tif (!startup_info->have_repository) {\n+\t\tif (opts->use_local_config)\n+\t\t\tdie(_(\"--local can only be used inside a git repository\"));\n+\t\tif (opts->source.blob)\n+\t\t\tdie(_(\"--blob can only be used inside a git repository\"));\n+\t\tif (opts->use_worktree_config)\n+\t\t\tdie(_(\"--worktree can only be used inside a git repository\"));\n+\t}\n+\n+\tif (opts->source.file &&\n+\t\t\t!strcmp(opts->source.file, \"-\")) {\n+\t\topts->source.file = NULL;\n+\t\topts->source.use_stdin = 1;\n+\t\topts->source.scope = CONFIG_SCOPE_COMMAND;\n+\t}\n+\n+\tif (opts->use_global_config) {\n+\t\topts->source.file = opts->file_to_free = git_global_config();\n+\t\tif (!opts->source.file)\n+\t\t\t/*\n+\t\t\t * It is unknown if HOME/.gitconfig exists, so\n+\t\t\t * we do not know if we should write to XDG\n+\t\t\t * location; error out even if XDG_CONFIG_HOME\n+\t\t\t * is set and points at a sane location.\n+\t\t\t */\n+\t\t\tdie(_(\"$HOME not set\"));\n+\t\topts->source.scope = CONFIG_SCOPE_GLOBAL;\n+\t} else if (opts->use_system_config) {\n+\t\topts->source.file = opts->file_to_free = git_system_config();\n+\t\topts->source.scope = CONFIG_SCOPE_SYSTEM;\n+\t} else if (opts->use_local_config) {\n+\t\topts->source.file = opts->file_to_free = repo_git_path(repo, \"config\");\n+\t\topts->source.scope = CONFIG_SCOPE_LOCAL;\n+\t} else if (opts->use_worktree_config) {\n+\t\tstruct worktree **worktrees = get_worktrees();\n+\t\tif (repo->repository_format_worktree_config)\n+\t\t\topts->source.file = opts->file_to_free =\n+\t\t\t\trepo_git_path(repo, \"config.worktree\");\n+\t\telse if (worktrees[0] && worktrees[1])\n+\t\t\tdie(_(\"--worktree cannot be used with multiple \"\n+\t\t\t      \"working trees unless the config\\n\"\n+\t\t\t      \"extension worktreeConfig is enabled. \"\n+\t\t\t      \"Please read \\\"CONFIGURATION FILE\\\"\\n\"\n+\t\t\t      \"section in \\\"git help worktree\\\" for details\"));\n+\t\telse\n+\t\t\topts->source.file = opts->file_to_free =\n+\t\t\t\trepo_git_path(repo, \"config\");\n+\t\topts->source.scope = CONFIG_SCOPE_LOCAL;\n+\t\tfree_worktrees(worktrees);\n+\t} else if (opts->source.file) {\n+\t\tif (!is_absolute_path(opts->source.file) && prefix)\n+\t\t\topts->source.file = opts->file_to_free =\n+\t\t\t\tprefix_filename(prefix, opts->source.file);\n+\t\topts->source.scope = CONFIG_SCOPE_COMMAND;\n+\t} else if (opts->source.blob) {\n+\t\topts->source.scope = CONFIG_SCOPE_COMMAND;\n+\t}\n+\n+\tif (opts->respect_includes_opt == -1)\n+\t\topts->options.respect_includes = !opts->source.file;\n+\telse\n+\t\topts->options.respect_includes = opts->respect_includes_opt;\n+\tif (startup_info->have_repository) {\n+\t\topts->options.commondir = repo_get_common_dir(repo);\n+\t\topts->options.git_dir = repo_get_git_dir(repo);\n+\t}\n+}\n+\n+void location_options_release(struct config_location_options *opts)\n+{\n+\tfree(opts->file_to_free);\n+}\ndiff --git a/config.h b/config.h\nindex 966a228f0e..6663964977 100644\n--- a/config.h\n+++ b/config.h\n@@ -166,6 +166,26 @@ struct config_context {\n typedef int (*config_fn_t)(const char *, const char *,\n \t\t\t   const struct config_context *, void *);\n \n+struct config_location_options {\n+\tstruct git_config_source source;\n+\tstruct config_options options;\n+\tchar *file_to_free;\n+\tint use_global_config;\n+\tint use_system_config;\n+\tint use_local_config;\n+\tint use_worktree_config;\n+\tint respect_includes_opt;\n+};\n+#define CONFIG_LOCATION_OPTIONS_INIT { \\\n+\t.respect_includes_opt = -1, \\\n+}\n+\n+void location_options_init(struct repository *repo,\n+\t\t\t   struct config_location_options *opts,\n+\t\t\t   const char *prefix);\n+\n+void location_options_release(struct config_location_options *opts);\n+\n /**\n  * Read a specific file in git-config format.\n  * This function takes the same callback and data parameters as `repo_config`.\n-- \ngitgitgadget\n\n"},{"id":"535157","messageId":"60443c56f456ca794e299ae8d8bbea23793780b5.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 08/11] config-batch: pass prefix through commands","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:20:00Z","receivedAt":"2026-02-04T14:20:19Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThe 'help' and 'get' commands of 'git config-batch' have not needed the\nprefix parameter from the builtin entrance point, but an upcoming\ncommand will need it in order to identify the location of the\nappropriate config file. Pass it through the appropriate functions and\nfunction pointers.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n builtin/config-batch.c | 26 +++++++++++++++++---------\n 1 file changed, 17 insertions(+), 9 deletions(-)\n\ndiff --git a/builtin/config-batch.c b/builtin/config-batch.c\nindex 2c48c4ea37..9829b16c6f 100644\n--- a/builtin/config-batch.c\n+++ b/builtin/config-batch.c\n@@ -67,9 +67,11 @@ static int command_parse_error(const char *command)\n  * Return 0 on success.\n  */\n typedef int (*command_fn)(struct repository *repo,\n+\t\t\t  const char *prefix,\n \t\t\t  char *data, size_t data_len);\n \n static int unknown_command(struct repository *repo UNUSED,\n+\t\t\t   const char *prefix UNUSED,\n \t\t\t   char *data UNUSED, size_t data_len UNUSED)\n {\n \treturn emit_response(UNKNOWN_COMMAND, NULL);\n@@ -176,6 +178,7 @@ static size_t parse_token(char **data, size_t *data_len,\n }\n \n static int help_command_1(struct repository *repo,\n+\t\t\t  const char *prefix UNUSED,\n \t\t\t  char *data, size_t data_len);\n \n enum value_match_mode {\n@@ -292,6 +295,7 @@ static int parse_scope(const char *str, enum config_scope *scope)\n  * [N*] indicates optional parameters that are not needed.\n  */\n static int get_command_1(struct repository *repo,\n+\t\t\t const char *prefix UNUSED,\n \t\t\t char *data,\n \t\t\t size_t data_len)\n {\n@@ -402,6 +406,7 @@ static struct command commands[] = {\n #define COMMAND_COUNT ((size_t)(sizeof(commands) / sizeof(*commands)))\n \n static int help_command_1(struct repository *repo UNUSED,\n+\t\t\t  const char *prefix UNUSED,\n \t\t\t  char *data UNUSED, size_t data_len UNUSED)\n {\n \tstruct strbuf fmt_str = STRBUF_INIT;\n@@ -424,7 +429,8 @@ static int help_command_1(struct repository *repo UNUSED,\n \treturn 0;\n }\n \n-static int process_command_nul(struct repository *repo)\n+static int process_command_nul(struct repository *repo,\n+\t\t\t       const char *prefix)\n {\n \tstatic struct strbuf line = STRBUF_INIT;\n \tchar *data, *command, *versionstr;\n@@ -476,7 +482,7 @@ static int process_command_nul(struct repository *repo)\n \t\tif (!commands[i].name[0] ||\n \t\t    (!strcmp(command, commands[i].name) &&\n \t\t     commands[i].version == version)) {\n-\t\t\tres = commands[i].fn(repo, data, data_len);\n+\t\t\tres = commands[i].fn(repo, prefix, data, data_len);\n \t\t\tgoto cleanup;\n \t\t}\n \t}\n@@ -484,14 +490,15 @@ static int process_command_nul(struct repository *repo)\n \tBUG(_(\"scanned to end of command list, including 'unknown_command'\"));\n \n parse_error:\n-\tres = unknown_command(repo, NULL, 0);\n+\tres = unknown_command(repo, prefix, NULL, 0);\n \n cleanup:\n \tstrbuf_release(&line);\n \treturn res;\n }\n \n-static int process_command_whitespace(struct repository *repo)\n+static int process_command_whitespace(struct repository *repo,\n+\t\t\t\t      const char *prefix)\n {\n \tstatic struct strbuf line = STRBUF_INIT;\n \tstruct string_list tokens = STRING_LIST_INIT_NODUP;\n@@ -536,7 +543,7 @@ static int process_command_whitespace(struct repository *repo)\n \t\tif (!commands[i].name[0] ||\n \t\t    (!strcmp(command, commands[i].name) &&\n \t\t     commands[i].version == version)) {\n-\t\t\tres = commands[i].fn(repo, data, data_len);\n+\t\t\tres = commands[i].fn(repo, prefix, data, data_len);\n \t\t\tgoto cleanup;\n \t\t}\n \t}\n@@ -559,11 +566,12 @@ cleanup:\n  *\n  * Returns negative value on other catastrophic error.\n  */\n-static int process_command(struct repository *repo)\n+static int process_command(struct repository *repo,\n+\t\t\t   const char *prefix)\n {\n \tif (zformat)\n-\t\treturn process_command_nul(repo);\n-\treturn process_command_whitespace(repo);\n+\t\treturn process_command_nul(repo, prefix);\n+\treturn process_command_whitespace(repo, prefix);\n }\n \n int cmd_config_batch(int argc,\n@@ -586,7 +594,7 @@ int cmd_config_batch(int argc,\n \n \trepo_config(repo, git_default_config, NULL);\n \n-\twhile (!(res = process_command(repo)));\n+\twhile (!(res = process_command(repo, prefix)));\n \n \tif (res == 1)\n \t\treturn 0;\n-- \ngitgitgadget\n\n"},{"id":"535158","messageId":"fdeef536f649bec811e8335d1c7151be8e352ff0.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 09/11] config-batch: add 'set' v1 command","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:20:01Z","receivedAt":"2026-02-04T14:20:21Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThis new command is intended for single-value assignments to a specific\nchosen scope. More complicated versions of the 'git config set' command\nwill be incorporated into future commands.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-config-batch.adoc | 24 ++++++++\n builtin/config-batch.c              | 71 ++++++++++++++++++++++\n config.c                            | 27 +++++++++\n config.h                            |  3 +\n t/t1312-config-batch.sh             | 94 ++++++++++++++++++++++++++++-\n 5 files changed, 217 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\nindex 3c9a3bb763..feec85c4ef 100644\n--- a/Documentation/git-config-batch.adoc\n+++ b/Documentation/git-config-batch.adoc\n@@ -111,6 +111,30 @@ get 1 missing <key> [<value-pattern>|<value>]\n where `<value-pattern>` or `<value>` is only supplied if provided in\n the command.\n \n+`set` version 1::\n+\tThe `set` command writes a single key-value pair to a config\n+\tfile. It specifies which file by a `<scope>` parameter from\n+\tamong `system`, `global`, `local`, and `worktree`. The `<key>`\n+\tis the next positional argument. The remaining data in the line\n+\tis provided as the `<value>` to assign the config.\n++\n+------------\n+set 1 <scope> <key> <value>\n+------------\n++\n+These uses will match the behavior of `git config --set --<scope> <key>\n+<value>`. Note that replacing all values with the `--all` option or\n+matching specific value patterns are not supported by this command.\n++\n+The response of these commands will include a `success` message if the\n+value is written as expected or `failed` if an unexpected failure\n+occurs:\n++\n+------------\n+set 1 success <scope> <key> <value>\n+set 1 failed <scope> <key> <value>\n+------------\n+\n NUL-Terminated Format\n ~~~~~~~~~~~~~~~~~~~~~\n \ndiff --git a/builtin/config-batch.c b/builtin/config-batch.c\nindex 9829b16c6f..373b0cad47 100644\n--- a/builtin/config-batch.c\n+++ b/builtin/config-batch.c\n@@ -16,6 +16,7 @@ static int zformat = 0;\n #define UNKNOWN_COMMAND \"unknown_command\"\n #define HELP_COMMAND \"help\"\n #define GET_COMMAND \"get\"\n+#define SET_COMMAND \"set\"\n #define COMMAND_PARSE_ERROR \"command_parse_error\"\n \n static void print_word(const char *word, int start)\n@@ -379,6 +380,71 @@ cleanup:\n \treturn res;\n }\n \n+\n+/**\n+ * 'set' command, version 1.\n+ *\n+ * Positional arguments should be of the form:\n+ *\n+ * [0] scope (\"system\", \"global\", \"local\", or \"worktree\")\n+ * [1] config key\n+ * [2] config value\n+ */\n+static int set_command_1(struct repository *repo,\n+\t\t\t const char *prefix,\n+\t\t\t char *data,\n+\t\t\t size_t data_len)\n+{\n+\tint res = 0, err = 0;\n+\tenum config_scope scope = CONFIG_SCOPE_UNKNOWN;\n+\tchar *token = NULL, *key = NULL, *value = NULL;\n+\tstruct config_location_options locopts = CONFIG_LOCATION_OPTIONS_INIT;\n+\n+\tif (!parse_token(&data, &data_len, &token, &err) || err)\n+\t\tgoto parse_error;\n+\n+\tif (parse_scope(token, &scope) ||\n+\t    scope == CONFIG_SCOPE_UNKNOWN ||\n+\t    scope == CONFIG_SCOPE_SUBMODULE ||\n+\t    scope == CONFIG_SCOPE_COMMAND)\n+\t\tgoto parse_error;\n+\n+\tif (!parse_token(&data, &data_len, &key, &err) || err)\n+\t\tgoto parse_error;\n+\n+\t/* Use the remaining data as the value string. */\n+\tif (!zformat)\n+\t\tvalue = data;\n+\telse {\n+\t\tparse_token(&data, &data_len, &value, &err);\n+\t\tif (err)\n+\t\t\tgoto parse_error;\n+\t}\n+\n+\tif (location_options_set_scope(&locopts, scope))\n+\t\tgoto parse_error;\n+\tlocation_options_init(repo, &locopts, prefix);\n+\n+\tres = repo_config_set_in_file_gently(repo, locopts.source.file,\n+\t\t\t\t\t     key, NULL, value);\n+\n+\tif (res)\n+\t\tres = emit_response(SET_COMMAND, \"1\", \"failure\",\n+\t\t\t\t    scope_str(scope), key, value, NULL);\n+\telse\n+\t\tres = emit_response(SET_COMMAND, \"1\", \"success\",\n+\t\t\t\t    scope_str(scope), key, value, NULL);\n+\n+\tgoto cleanup;\n+\n+parse_error:\n+\tres = command_parse_error(SET_COMMAND);\n+\n+cleanup:\n+\tlocation_options_release(&locopts);\n+\treturn res;\n+}\n+\n struct command {\n \tconst char *name;\n \tcommand_fn fn;\n@@ -396,6 +462,11 @@ static struct command commands[] = {\n \t\t.fn = get_command_1,\n \t\t.version = 1,\n \t},\n+\t{\n+\t\t.name = SET_COMMAND,\n+\t\t.fn = set_command_1,\n+\t\t.version = 1,\n+\t},\n \t/* unknown_command must be last. */\n \t{\n \t\t.name = \"\",\ndiff --git a/config.c b/config.c\nindex 9f1a7b45cf..fa72234750 100644\n--- a/config.c\n+++ b/config.c\n@@ -3594,6 +3594,33 @@ int lookup_config(const char **mapping, int nr_mapping, const char *var)\n \treturn -1;\n }\n \n+int location_options_set_scope(struct config_location_options *opts,\n+\t\t\t       enum config_scope scope)\n+{\n+\tswitch (scope) {\n+\tcase CONFIG_SCOPE_SYSTEM:\n+\t\topts->use_system_config = 1;\n+\t\tbreak;\n+\n+\tcase CONFIG_SCOPE_GLOBAL:\n+\t\topts->use_global_config = 1;\n+\t\tbreak;\n+\n+\tcase CONFIG_SCOPE_LOCAL:\n+\t\topts->use_local_config = 1;\n+\t\tbreak;\n+\n+\tcase CONFIG_SCOPE_WORKTREE:\n+\t\topts->use_worktree_config = 1;\n+\t\tbreak;\n+\n+\tdefault:\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n void location_options_init(struct repository *repo,\n \t\t\t   struct config_location_options *opts,\n \t\t\t   const char *prefix)\ndiff --git a/config.h b/config.h\nindex 6663964977..f6432c1ec2 100644\n--- a/config.h\n+++ b/config.h\n@@ -180,6 +180,9 @@ struct config_location_options {\n \t.respect_includes_opt = -1, \\\n }\n \n+int location_options_set_scope(struct config_location_options *opts,\n+\t\t\t       enum config_scope scope);\n+\n void location_options_init(struct repository *repo,\n \t\t\t   struct config_location_options *opts,\n \t\t\t   const char *prefix);\ndiff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh\nindex f7a74ddc2c..40f6f90ef2 100755\n--- a/t/t1312-config-batch.sh\n+++ b/t/t1312-config-batch.sh\n@@ -47,9 +47,10 @@ test_expect_success 'help command' '\n \techo \"help 1\" >in &&\n \n \tcat >expect <<-\\EOF &&\n-\thelp 1 count 2\n+\thelp 1 count 3\n \thelp 1 help 1\n \thelp 1 get 1\n+\thelp 1 set 1\n \tEOF\n \n \tgit config-batch >out <in &&\n@@ -63,9 +64,10 @@ test_expect_success 'help -z' '\n \tEOF\n \n \tcat >expect <<-\\EOF &&\n-\t4:help 1:1 5:count 1:2\n+\t4:help 1:1 5:count 1:3\n \t4:help 1:1 4:help 1:1\n \t4:help 1:1 3:get 1:1\n+\t4:help 1:1 3:set 1:1\n \t15:unknown_command\n \tEOF\n \n@@ -205,4 +207,92 @@ test_expect_success 'get config with -z' '\n \ttest_cmp expect out\n '\n \n+test_expect_success 'set config by scope' '\n+\ttest_when_finished git config remove-section test.set &&\n+\tGIT_CONFIG_SYSTEM=system-config-file &&\n+\tGIT_CONFIG_NOSYSTEM=0 &&\n+\tGIT_CONFIG_GLOBAL=global-config-file &&\n+\texport GIT_CONFIG_SYSTEM &&\n+\texport GIT_CONFIG_NOSYSTEM &&\n+\texport GIT_CONFIG_GLOBAL &&\n+\n+\tcat >in <<-\\EOF &&\n+\tset 1 system test.set.system system\n+\tset 1 global test.set.global global\n+\tset 1 local test.set.local local with spaces\n+\tset 1 worktree test.set.worktree worktree\n+\tset 1 submodule test.set.submodule submodule\n+\tset 1 command test.set.command command\n+\tset 1 inherited test.set.inherited inherited\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tset 1 success system test.set.system system\n+\tset 1 success global test.set.global global\n+\tset 1 success local test.set.local local with spaces\n+\tset 1 success worktree test.set.worktree worktree\n+\tcommand_parse_error set\n+\tcommand_parse_error set\n+\tcommand_parse_error set\n+\tEOF\n+\n+\tgit config-batch <in >out 2>err &&\n+\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect out &&\n+\n+\tcat >expect-values <<-EOF &&\n+\tfile:system-config-file\tsystem\n+\tfile:global-config-file\tglobal\n+\tfile:.git/config\tlocal with spaces\n+\tfile:.git/config.worktree\tworktree\n+\tEOF\n+\n+\tgit config get --show-origin --regexp --all test.set.* >values &&\n+\ttest_cmp expect-values values\n+'\n+\n+test_expect_success 'set config by scope with -z' '\n+\ttest_when_finished git config remove-section test.set &&\n+\tGIT_CONFIG_SYSTEM=system-config-file &&\n+\tGIT_CONFIG_NOSYSTEM=0 &&\n+\tGIT_CONFIG_GLOBAL=global-config-file &&\n+\texport GIT_CONFIG_SYSTEM &&\n+\texport GIT_CONFIG_NOSYSTEM &&\n+\texport GIT_CONFIG_GLOBAL &&\n+\n+\tcat >in <<-\\EOF &&\n+\t3:set NUL 1:1 NUL 6:system NUL 15:test.set.system NUL 6:system NUL NUL\n+\t3:set NUL 1:1 NUL 6:global NUL 15:test.set.global NUL 6:global NUL NUL\n+\t3:set NUL 1:1 NUL 5:local NUL 14:test.set.local NUL 17:local with spaces NUL NUL\n+\t3:set NUL 1:1 NUL 8:worktree NUL 17:test.set.worktree NUL 8:worktree NUL NUL\n+\t3:set NUL 1:1 NUL 9:submodule NUL 18:test.set.submodule NUL 9:submodule NUL NUL\n+\t3:set NUL 1:1 NUL 7:command NUL 16:test.set.command NUL 7:command NUL NUL\n+\t3:set NUL 1:1 NUL 9:inherited NUL 18:test.set.inherited NUL 9:inherited NUL NUL\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\t3:set 1:1 7:success 6:system 15:test.set.system 6:system\n+\t3:set 1:1 7:success 6:global 15:test.set.global 6:global\n+\t3:set 1:1 7:success 5:local 14:test.set.local 17:local with spaces\n+\t3:set 1:1 7:success 8:worktree 17:test.set.worktree 8:worktree\n+\t19:command_parse_error 3:set\n+\t19:command_parse_error 3:set\n+\t19:command_parse_error 3:set\n+\tEOF\n+\n+\ttest_zformat git config-batch -z >out <in &&\n+\ttest_cmp expect out &&\n+\n+\tcat >expect-values <<-EOF &&\n+\tfile:system-config-file\tsystem\n+\tfile:global-config-file\tglobal\n+\tfile:.git/config\tlocal with spaces\n+\tfile:.git/config.worktree\tworktree\n+\tEOF\n+\n+\tgit config get --show-origin --regexp --all test.set.* >values &&\n+\ttest_cmp expect-values values\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"535159","messageId":"cf4f054fb6d382875402511b49ee901486380476.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 10/11] t1312: create read/write test","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:20:02Z","receivedAt":"2026-02-04T14:20:22Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nThis new test will be extended in the future to ensure that multiple\ncommands that execute in order update the configuration state enough to\nreflect new written values as we read them in later commands.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n t/t1312-config-batch.sh | 27 +++++++++++++++++++++++++++\n 1 file changed, 27 insertions(+)\n\ndiff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh\nindex 40f6f90ef2..11380f4247 100755\n--- a/t/t1312-config-batch.sh\n+++ b/t/t1312-config-batch.sh\n@@ -295,4 +295,31 @@ test_expect_success 'set config by scope with -z' '\n \ttest_cmp expect-values values\n '\n \n+test_expect_success 'read/write interactions in sequence' '\n+\ttest_when_finished git config remove-section test.rw &&\n+\n+\tcat >in <<-\\EOF &&\n+\tget 1 local test.rw.missing\n+\tset 1 local test.rw.found found\n+\tget 1 local test.rw.found\n+\tset 1 local test.rw.found updated\n+\tget 1 local test.rw.found\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tget 1 missing test.rw.missing\n+\tset 1 success local test.rw.found found\n+\tget 1 found test.rw.found local found\n+\tset 1 success local test.rw.found updated\n+\tget 1 found test.rw.found local updated\n+\tEOF\n+\n+\tgit config-batch <in >out 2>err &&\n+\n+\ttest_must_be_empty err &&\n+\ttest_cmp expect out &&\n+\n+\ttest_cmp_config updated test.rw.found\n+'\n+\n test_done\n-- \ngitgitgadget\n\n"},{"id":"535160","messageId":"59d19fee5f5bd34c5864bebb8243afdc6bc9ea7a.1770214803.git.gitgitgadget@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"[PATCH 11/11] config-batch: add unset v1 command","fromName":"Derrick Stolee via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-02-04T14:20:03Z","receivedAt":"2026-02-04T14:20:24Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"From: Derrick Stolee <stolee@gmail.com>\n\nAdd a new 'unset' command with version 1 that mimics 'git config\n--unset' with optional regex pattern or '--fixed-value' arguments.\n\nSigned-off-by: Derrick Stolee <stolee@gmail.com>\n---\n Documentation/git-config-batch.adoc | 28 ++++++++\n builtin/config-batch.c              | 99 +++++++++++++++++++++++++++++\n t/t1312-config-batch.sh             | 61 ++++++++++++++++--\n 3 files changed, 181 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\nindex feec85c4ef..bdfd872d65 100644\n--- a/Documentation/git-config-batch.adoc\n+++ b/Documentation/git-config-batch.adoc\n@@ -135,6 +135,34 @@ set 1 success <scope> <key> <value>\n set 1 failed <scope> <key> <value>\n ------------\n \n+`unset` version 1::\n+\tThe `unset` command removes a single value from a config file.\n+\tIt specifies which file by a `<scope>` parameter from among\n+\t`system`, `global`, `local`, and `worktree`. The `<key>` is the\n+\tnext positional argument. There could be two additional\n+\targuments used to match specific config values, where the first\n+\tis either `arg:regex` or `arg:fixed-value` to specify the type\n+\tof match.\n++\n+------------\n+unset 1 <scope> <key>\n+unset 1 <scope> <key> arg:regex <value-pattern>\n+unset 1 <scope> <key> arg:fixed-value <value>\n+------------\n++\n+These uses will match the behavior of `git config --unset --<scope> <key>`\n+with the additional arguments of `<value-pattern>` if `arg:regex` is\n+given or `--fixed-value <value>` if `arg:fixed-value` is given.\n++\n+The response of these commands will include a `success` message\n+if matched values are found and removed as expected or `failed` if an\n+unexpected failure occurs:\n++\n+------------\n+unset 1 success <scope> <key>\n+unset 1 failed <scope> <key>\n+------------\n+\n NUL-Terminated Format\n ~~~~~~~~~~~~~~~~~~~~~\n \ndiff --git a/builtin/config-batch.c b/builtin/config-batch.c\nindex 373b0cad47..25a942ba61 100644\n--- a/builtin/config-batch.c\n+++ b/builtin/config-batch.c\n@@ -17,6 +17,7 @@ static int zformat = 0;\n #define HELP_COMMAND \"help\"\n #define GET_COMMAND \"get\"\n #define SET_COMMAND \"set\"\n+#define UNSET_COMMAND \"unset\"\n #define COMMAND_PARSE_ERROR \"command_parse_error\"\n \n static void print_word(const char *word, int start)\n@@ -445,6 +446,99 @@ cleanup:\n \treturn res;\n }\n \n+/**\n+ * 'unset' command, version 1.\n+ *\n+ * Positional arguments should be of the form:\n+ *\n+ * [0] scope (\"system\", \"global\", \"local\", or \"worktree\")\n+ * [1] config key\n+ * [2] config value\n+ * [3*] match (\"regex\", \"fixed-value\")\n+ * [4*] value regex OR value string\n+ *\n+ * [N*] indicates optional parameters that are not needed.\n+ */\n+static int unset_command_1(struct repository *repo,\n+\t\t\t const char *prefix,\n+\t\t\t char *data,\n+\t\t\t size_t data_len)\n+{\n+\tint res = 0, err = 0, flags = 0;\n+\tenum config_scope scope = CONFIG_SCOPE_UNKNOWN;\n+\tchar *token = NULL, *key = NULL, *value_pattern = NULL;\n+\tsize_t token_len;\n+\tstruct config_location_options locopts = CONFIG_LOCATION_OPTIONS_INIT;\n+\n+\tif (!parse_token(&data, &data_len, &token, &err) || err)\n+\t\tgoto parse_error;\n+\n+\tif (parse_scope(token, &scope) ||\n+\t    scope == CONFIG_SCOPE_UNKNOWN ||\n+\t    scope == CONFIG_SCOPE_SUBMODULE ||\n+\t    scope == CONFIG_SCOPE_COMMAND)\n+\t\tgoto parse_error;\n+\n+\tif (!parse_token(&data, &data_len, &key, &err) || err)\n+\t\tgoto parse_error;\n+\n+\ttoken_len = parse_token(&data, &data_len, &token, &err);\n+\tif (err)\n+\t\tgoto parse_error;\n+\n+\tif (token_len && !strncmp(token, \"arg:\", 4)) {\n+\t\tif (!strcmp(token + 4, \"fixed-value\"))\n+\t\t\tflags |= CONFIG_FLAGS_FIXED_VALUE;\n+\t\t/* no special logic for arg:regex. */\n+\t\telse if (strcmp(token + 4, \"regex\"))\n+\t\t\tgoto parse_error; /* unknown arg. */\n+\n+\t\t/* Use the remaining data as the value string. */\n+\t\tif (!zformat)\n+\t\t\tvalue_pattern = data;\n+\t\telse {\n+\t\t\tparse_token(&data, &data_len, &value_pattern, &err);\n+\t\t\tif (err)\n+\t\t\t\tgoto parse_error;\n+\t\t}\n+\t} else if (token_len) {\n+\t\t/*\n+\t\t * If we have remaining tokens not starting in \"arg:\",\n+\t\t * then we don't understand them.\n+\t\t */\n+\t\tgoto parse_error;\n+\t}\n+\n+\tif (location_options_set_scope(&locopts, scope))\n+\t\tgoto parse_error;\n+\tlocation_options_init(repo, &locopts, prefix);\n+\n+\tres = repo_config_set_multivar_in_file_gently(\n+\t\t\trepo,\n+\t\t\tlocopts.source.file,\n+\t\t\tkey,\n+\t\t\t/* value */ NULL,\n+\t\t\tvalue_pattern,\n+\t\t\t/* comment */ NULL,\n+\t\t\tflags);\n+\n+\tif (res)\n+\t\tres = emit_response(UNSET_COMMAND, \"1\", \"failure\",\n+\t\t\t\t    scope_str(scope), key, NULL);\n+\telse\n+\t\tres = emit_response(UNSET_COMMAND, \"1\", \"success\",\n+\t\t\t\t    scope_str(scope), key, NULL);\n+\n+\tgoto cleanup;\n+\n+parse_error:\n+\tres = command_parse_error(UNSET_COMMAND);\n+\n+cleanup:\n+\tlocation_options_release(&locopts);\n+\treturn res;\n+}\n+\n struct command {\n \tconst char *name;\n \tcommand_fn fn;\n@@ -467,6 +561,11 @@ static struct command commands[] = {\n \t\t.fn = set_command_1,\n \t\t.version = 1,\n \t},\n+\t{\n+\t\t.name = UNSET_COMMAND,\n+\t\t.fn = unset_command_1,\n+\t\t.version = 1,\n+\t},\n \t/* unknown_command must be last. */\n \t{\n \t\t.name = \"\",\ndiff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh\nindex 11380f4247..3bddbc0de3 100755\n--- a/t/t1312-config-batch.sh\n+++ b/t/t1312-config-batch.sh\n@@ -47,10 +47,11 @@ test_expect_success 'help command' '\n \techo \"help 1\" >in &&\n \n \tcat >expect <<-\\EOF &&\n-\thelp 1 count 3\n+\thelp 1 count 4\n \thelp 1 help 1\n \thelp 1 get 1\n \thelp 1 set 1\n+\thelp 1 unset 1\n \tEOF\n \n \tgit config-batch >out <in &&\n@@ -64,10 +65,11 @@ test_expect_success 'help -z' '\n \tEOF\n \n \tcat >expect <<-\\EOF &&\n-\t4:help 1:1 5:count 1:3\n+\t4:help 1:1 5:count 1:4\n \t4:help 1:1 4:help 1:1\n \t4:help 1:1 3:get 1:1\n \t4:help 1:1 3:set 1:1\n+\t4:help 1:1 5:unset 1:1\n \t15:unknown_command\n \tEOF\n \n@@ -295,15 +297,60 @@ test_expect_success 'set config by scope with -z' '\n \ttest_cmp expect-values values\n '\n \n-test_expect_success 'read/write interactions in sequence' '\n-\ttest_when_finished git config remove-section test.rw &&\n+test_expect_success 'unset config by scope and filter' '\n+\tGIT_CONFIG_SYSTEM=system-config-file &&\n+\tGIT_CONFIG_NOSYSTEM=0 &&\n+\tGIT_CONFIG_GLOBAL=global-config-file &&\n+\texport GIT_CONFIG_SYSTEM &&\n+\texport GIT_CONFIG_NOSYSTEM &&\n+\texport GIT_CONFIG_GLOBAL &&\n+\n+\tcat >in <<-\\EOF &&\n+\tset 1 system test.unset.key system\n+\tset 1 global test.unset.key global\n+\tset 1 local test.unset.key local with spaces\n+\tset 1 worktree test.unset.key worktree\n+\tunset 1 system test.unset.key\n+\tunset 1 global test.unset.key arg:regex g.*\n+\tunset 1 local test.unset.key arg:fixed-value local with spaces\n+\tunset 1 worktree test.unset.key arg:fixed-value submodule\n+\tunset 1 worktree test.unset.key arg:regex l.*\n+\tEOF\n+\n+\tcat >expect <<-\\EOF &&\n+\tset 1 success system test.unset.key system\n+\tset 1 success global test.unset.key global\n+\tset 1 success local test.unset.key local with spaces\n+\tset 1 success worktree test.unset.key worktree\n+\tunset 1 success system test.unset.key\n+\tunset 1 success global test.unset.key\n+\tunset 1 success local test.unset.key\n+\tunset 1 failure worktree test.unset.key\n+\tunset 1 failure worktree test.unset.key\n+\tEOF\n+\n+\tgit config-batch <in >out 2>err &&\n \n+\ttest_must_be_empty err &&\n+\ttest_cmp expect out &&\n+\n+\tcat >expect-values <<-EOF &&\n+\tfile:.git/config.worktree\tworktree\n+\tEOF\n+\n+\tgit config get --show-origin --regexp --all test.unset.key >values &&\n+\ttest_cmp expect-values values\n+'\n+\n+test_expect_success 'read/write interactions in sequence' '\n \tcat >in <<-\\EOF &&\n \tget 1 local test.rw.missing\n \tset 1 local test.rw.found found\n \tget 1 local test.rw.found\n \tset 1 local test.rw.found updated\n \tget 1 local test.rw.found\n+\tunset 1 local test.rw.found arg:fixed-value updated\n+\tget 1 local test.rw.found\n \tEOF\n \n \tcat >expect <<-\\EOF &&\n@@ -312,14 +359,14 @@ test_expect_success 'read/write interactions in sequence' '\n \tget 1 found test.rw.found local found\n \tset 1 success local test.rw.found updated\n \tget 1 found test.rw.found local updated\n+\tunset 1 success local test.rw.found\n+\tget 1 missing test.rw.found\n \tEOF\n \n \tgit config-batch <in >out 2>err &&\n \n \ttest_must_be_empty err &&\n-\ttest_cmp expect out &&\n-\n-\ttest_cmp_config updated test.rw.found\n+\ttest_cmp expect out\n '\n \n test_done\n-- \ngitgitgadget\n"},{"id":"535195","messageId":"xmqq5x8cnm93.fsf@gitster.g","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"Re: [PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-04T23:04:56Z","receivedAt":"2026-02-04T23:04:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> This RFC explores a new git config-batch builtin that allows tools to\n> interact with Git's config data with multiple queries using a single\n> process. This is an orthogonal alternative to the effort to create a stable,\n> linkable config API. Both approaches have different strengths.\n\nJust a few random thoughts before diving into the patches.\n\n> My main motivation is the performance of git-credential-manager on Windows\n> platforms as it can call git config get dozens of times. At 150-200ms per\n> execution, that adds up significantly, leading to multiple seconds just to\n> load a credential that already exists. I believe that there are other\n> benefits to having this interface available, but I can't recall any\n> specifics at the moment.\n\nSo this would be \"credential-manager gets started, and instead of\nhaving to spawn 'git config' many times, spawn a single instance of\n'git config --batch' and talk with it\".  Would it be beneficial to\nfurther think about a long-running 'git config --server' that can be\ncontacted by a credential-manager (or other processes) whose lifetime\nis totally independent, possibly over local transport mechanisms\nlike named pipes, or is it a key to keep the mechanism and design\nsimple to limit the number of customer this service supports to only\none at a time and we would prefer to keep it that way?\n\n> One thing that I think would be valuable to include is a reload command that\n> signals that the git config-batch process should reload the configset into\n> memory due to config manipulations in other processes, especially while git\n> config-batch doesn't have all capabilities from git config. I'll include\n> that in the first version for review, if this RFC leads to positive support.\n\nCan \"git config --batch\" write/modify configuration, and if so, when\ndoes it make its modification available to the outside world?  Would\nwe have a \"flush\" command, or it would pretty much be immediate?\n\nCan we do without an explicit \"reload\" command by noticing when\nthe configuration files are updated and automatically reload?\n\nI am trying to figure out how more than one \"git config --batch\"\nprocesses can coordinate with each other with minimum overhead.  It\nis not a goal to have multiple such processes, but it would be a\ngoal to support multiple clients each of which would benefit from\nhaving access to the configuration data service (which is why I\nbrought up a single and shared long-running daemon as a possible\nalternative earlier).\n"},{"id":"535196","messageId":"xmqq1pj0nleg.fsf@gitster.g","threadId":"64916","inReplyTo":"c4dab0609613bc5d43bce705dca2f057674a5d5b.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 01/11] config-batch: basic boilerplate of new builtin","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-04T23:23:19Z","receivedAt":"2026-02-04T23:23:21Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> Later changes will document, implement, and test this new builtin. For now,\n> this serves as the latest example of the minimum boilerplate to introduce a\n> new builtin.\n>\n> Recently, we updated the comment in builtin.h about how to create a new\n> builtin, but failed to mention the required change to meson.build files for\n> some CI builds to pass. Fix that oversight.\n>\n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n\nWe have had a bad reputation for having too many commands; would it\nbe better to present it as a new mode of existing \"git config\"\ncommand at the end-user level, I wonder?\n\nAlso after reading patches for a few early steps, I do not quite see\n\"batch\"-ness in this protocol; it is strictly \"a single request is\nmet with a single response\".\n\n"},{"id":"535197","messageId":"xmqqv7gcm6p6.fsf@gitster.g","threadId":"64916","inReplyTo":"ecd26a0f1fad5615aea07a388e34f02e9f33b870.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 02/11] config-batch: create parse loop and unknown command","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-04T23:26:13Z","receivedAt":"2026-02-04T23:26:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> +static struct command commands[] = {\n> +\t/* unknown_command must be last. */\n> +\t{\n> +\t\t.name = \"\",\n> +\t\t.fn   = unknown_command,\n> +\t},\n> +};\n\nA useful trick is to deliberately omit the trailing comma after the\nelement that MUST be last.  You did that for the __NR enum element\nin a later step.\n\n> +#define COMMAND_COUNT ((size_t)(sizeof(commands) / sizeof(*commands)))\n\nIsn't this ARRAY_SIZE(commands)?\n\n\n> +\twhile (!(res = process_command(repo)));\n\nPlease write an empty statement on its own line, i.e.\n\n\twhile (!(res = process_command(repo)))\n\t\t;\n\n"},{"id":"535199","messageId":"aYPeiqkw41ln7De_@fruit.crustytoothpaste.net","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"Re: [PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2026-02-05T00:04:26Z","receivedAt":"2026-02-05T00:04:35Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"On 2026-02-04 at 14:19:52, Derrick Stolee via GitGitGadget wrote:\n> This RFC explores a new git config-batch builtin that allows tools to\n> interact with Git's config data with multiple queries using a single\n> process. This is an orthogonal alternative to the effort to create a stable,\n> linkable config API. Both approaches have different strengths.\n> \n> My main motivation is the performance of git-credential-manager on Windows\n> platforms as it can call git config get dozens of times. At 150-200ms per\n> execution, that adds up significantly, leading to multiple seconds just to\n> load a credential that already exists. I believe that there are other\n> benefits to having this interface available, but I can't recall any\n> specifics at the moment.\n> \n> This RFC adds git config-batch with a protocol over stdin/stdout for\n> executing multiple config queries. The implementation has a limited set of\n> potential queries, but also creates a model for compatibility for tools to\n> automatically adapt to different Git versions.\n> \n> I'm submitting this as an RFC before I've polished all of the details\n> because I want to make sure I'm going down a good direction. Please focus\n> feedback in these questions:\n> \n>  * Is this a worthwhile feature to add to Git?\n\nGit LFS has the same needs, but I believe it can use `git config -l -z`\nto do that and parse the config options itself.  If this is just config\nfetching, I'm not sure of the additional utility that such a feature\nwould add.  If that interface _almost_ meets your needs, could we add\nfunctionality there instead of a new interface?\n\nIf you need to set many keys, I'm curious as to why that is.\n-- \nbrian m. carlson (they/them)\nToronto, Ontario, CA\n"},{"id":"535241","messageId":"f6687192-58dd-479e-8df5-a422c01f03f4@gmail.com","threadId":"64916","inReplyTo":"aYPeiqkw41ln7De_@fruit.crustytoothpaste.net","subject":"Re: [PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-02-05T13:52:12Z","receivedAt":"2026-02-05T13:52:15Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 2/4/2026 7:04 PM, brian m. carlson wrote:\n> On 2026-02-04 at 14:19:52, Derrick Stolee via GitGitGadget wrote:\n>> This RFC explores a new git config-batch builtin that allows tools to\n>> interact with Git's config data with multiple queries using a single\n>> process. This is an orthogonal alternative to the effort to create a stable,\n>> linkable config API. Both approaches have different strengths.\n>>\n>> My main motivation is the performance of git-credential-manager on Windows\n>> platforms as it can call git config get dozens of times. At 150-200ms per\n>> execution, that adds up significantly, leading to multiple seconds just to\n>> load a credential that already exists. I believe that there are other\n>> benefits to having this interface available, but I can't recall any\n>> specifics at the moment.\n\n>>  * Is this a worthwhile feature to add to Git?\n> \n> Git LFS has the same needs, but I believe it can use `git config -l -z`\n> to do that and parse the config options itself.  If this is just config\n> fetching, I'm not sure of the additional utility that such a feature\n> would add.  If that interface _almost_ meets your needs, could we add\n> functionality there instead of a new interface?\n\nThis is a good suggestion to look into as a potentially-easier solution.\n\nThere may be some work required on the consumer to interpret multiple\nvalues and the right inheritance rules. This is relatively minor\ncompared to attempting a full parser with complicated 'includeIf'\nlogic.\n > If you need to set many keys, I'm curious as to why that is.\n\nI know that the credential manager does more than just query the config,\nbut also sets and unsets config. The full interface is here [1]. However,\nthe performance-critical parts may not require mutating configuration\nvalues, and hence such a \n\n[1] https://github.com/git-ecosystem/git-credential-manager/blob/main/src/shared/Core/GitConfiguration.cs#L31\n\nThanks for the pointer to git-lfs as a similar use case. I see that it\nhas a way to get the full list of config values [2] with '-l' (but not\n'-z'). It also has methods for getting values on a per-key (or even\nper-file) basis. I have not tracked the uses of config code into its\nconsumers to know how often one is used over the other.\n\n[2] https://github.com/git-lfs/git-lfs/blob/bb65882304a655ffa8abf2be6922e53ff18af5a5/git/config.go#L208\n\nThanks,\n-Stolee\n\n"},{"id":"535242","messageId":"36bae79b-576f-48f1-b31c-15c3a1f4bce7@gmail.com","threadId":"64916","inReplyTo":"xmqq5x8cnm93.fsf@gitster.g","subject":"Re: [PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-02-05T14:10:55Z","receivedAt":"2026-02-05T14:10:57Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 2/4/2026 6:04 PM, Junio C Hamano wrote:\n> \"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> This RFC explores a new git config-batch builtin that allows tools to\n>> interact with Git's config data with multiple queries using a single\n>> process. This is an orthogonal alternative to the effort to create a stable,\n>> linkable config API. Both approaches have different strengths.\n> \n> Just a few random thoughts before diving into the patches.\n> \n>> My main motivation is the performance of git-credential-manager on Windows\n>> platforms as it can call git config get dozens of times. At 150-200ms per\n>> execution, that adds up significantly, leading to multiple seconds just to\n>> load a credential that already exists. I believe that there are other\n>> benefits to having this interface available, but I can't recall any\n>> specifics at the moment.\n> \n> So this would be \"credential-manager gets started, and instead of\n> having to spawn 'git config' many times, spawn a single instance of\n> 'git config --batch' and talk with it\".  Would it be beneficial to\n> further think about a long-running 'git config --server' that can be\n> contacted by a credential-manager (or other processes) whose lifetime\n> is totally independent, possibly over local transport mechanisms\n> like named pipes, or is it a key to keep the mechanism and design\n> simple to limit the number of customer this service supports to only\n> one at a time and we would prefer to keep it that way?\n\nI could imagine a world where we have this approach, similar to the\nfsmonitor server. I should do more research and refresh my memory on that\nI/O model, if only to potentially reuse some of the parsing logic.\n\nBut this would be an interesting potential direction, saving the process\nstart-up time entirely.\n\nThe one big difficulty that I see is that the config will need to be\nrefreshed proactively by the server, potentially by watching the config\nfiles themselves (including any files included along the way) and also any\nchanges to repo state, such as the current branch. Any repo state that\ncould impact the 'includeIf' logic would need to be checked carefully. \n\n>> One thing that I think would be valuable to include is a reload command that\n>> signals that the git config-batch process should reload the configset into\n>> memory due to config manipulations in other processes, especially while git\n>> config-batch doesn't have all capabilities from git config. I'll include\n>> that in the first version for review, if this RFC leads to positive support.\n> \n> Can \"git config --batch\" write/modify configuration, and if so, when\n> does it make its modification available to the outside world?  Would\n> we have a \"flush\" command, or it would pretty much be immediate?\n\nThe 'set' command in this series calls methods that reach into\nrepo_config_set_multivar_in_file_gently() which updates the config file as\npart of that call, including using the .lock file technique to avoid\nconcurrent writes. Looking closely, it appears we do the right thing by\nparsing the existing file so we only update the new values while allowing\nany concurrent writes to the file to be respected, even if they disagree\nwith our current view of the config.\n\nSuch assignments also update our in-memory view _of those keys_ but it may\nbe a good time to automatically refresh the entire set of config values.\n\n> Can we do without an explicit \"reload\" command by noticing when\n> the configuration files are updated and automatically reload?\n\nThis would be an interesting approach, especially for the server concept.\n\n> I am trying to figure out how more than one \"git config --batch\"\n> processes can coordinate with each other with minimum overhead.  It\n> is not a goal to have multiple such processes, but it would be a\n> goal to support multiple clients each of which would benefit from\n> having access to the configuration data service (which is why I\n> brought up a single and shared long-running daemon as a possible\n> alternative earlier).\n\nYou're right to bring up these concerns. While 'git config-batch' is\nintended to be relatively short-lived, users could build tools that keep\nit alive for a long time. Thus, it is important to consider these\nautomatically-refreshing scenarios. And if we are automatically\nrefreshing, then should we instead consider a client/server model?\n\nI have some things to explore at the highest levels. I will likely start\nby exploring brian's 'git config -l -z' suggestion to see if that solves\nthe short-term need. But I will consider these other ideas to see where\nthey lead in terms of complexity and potential applications.\n\nThanks,\n-Stolee\n\n"},{"id":"535243","messageId":"6c8b984e-feda-48c6-b67d-80a41343bfc0@gmail.com","threadId":"64916","inReplyTo":"xmqq1pj0nleg.fsf@gitster.g","subject":"Re: [PATCH 01/11] config-batch: basic boilerplate of new builtin","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-02-05T14:17:58Z","receivedAt":"2026-02-05T14:18:01Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 2/4/2026 6:23 PM, Junio C Hamano wrote:\n> \"Derrick Stolee via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> From: Derrick Stolee <stolee@gmail.com>\n>>\n>> Later changes will document, implement, and test this new builtin. For now,\n>> this serves as the latest example of the minimum boilerplate to introduce a\n>> new builtin.\n>>\n>> Recently, we updated the comment in builtin.h about how to create a new\n>> builtin, but failed to mention the required change to meson.build files for\n>> some CI builds to pass. Fix that oversight.\n>>\n>> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n>> ---\n> \n> We have had a bad reputation for having too many commands; would it\n> be better to present it as a new mode of existing \"git config\"\n> command at the end-user level, I wonder?\n\nInteresting thought. I think we also have a bad reputation of commands\nthat are overloaded with too many purposes.\n\nIn this case, though, I do think that the modern 'git config <subcommand>'\nmodel presents some clear boundaries for how the command should behave\nwith the 'batch' (or 'server') subcommand. Grouping all config-related\noperations in the same builtin may be ideal. \n\n> Also after reading patches for a few early steps, I do not quite see\n> \"batch\"-ness in this protocol; it is strictly \"a single request is\n> met with a single response\".\n\nThe batch-ness is that multiple requests can eventually go to the same\nprocess. The client could collect multiple commands in a batch and send\nthem all without processing the responses one-by-one. This is how it works\nin the tests: a single input file is prepared and all responses are\nscanned after-the-fact.\n\nThe back-and-forth mechanism is how the git-credential-manager tool would\nuse it, because it dynamically explores certain config keys. For example:\nit checks the deepest possible URL for a specific key then peels away the\nlast segment of the URL to see if there is a directory-prefix match in a\nkey. (This is the main reason that there are so many requests in this\napplication.)\n\nI believe this is similar to how 'git cat-file --batch' or 'git cat-file\n--batch-check' work, which was my inspiration for this word. If we regret\nthose names, then I'm happy to move towards a better name.\n\nThanks,\n-Stolee\n"},{"id":"535244","messageId":"55e2e1f0-da51-4413-a20b-542140004fb6@gmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"Re: [PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-02-05T14:45:40Z","receivedAt":"2026-02-05T14:45:43Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Stolee\n\nOn 04/02/2026 14:19, Derrick Stolee via GitGitGadget wrote:\n> This RFC explores a new git config-batch builtin that allows tools to\n> interact with Git's config data with multiple queries using a single\n> process. This is an orthogonal alternative to the effort to create a stable,\n> linkable config API. Both approaches have different strengths.\n> \n> My main motivation is the performance of git-credential-manager on Windows\n> platforms as it can call git config get dozens of times. At 150-200ms per\n> execution, that adds up significantly, leading to multiple seconds just to\n> load a credential that already exists. I believe that there are other\n> benefits to having this interface available, but I can't recall any\n> specifics at the moment.\n\nIt would be helpful to explain what the advantage of this new command is \nover using \"git config --list -z\" or \"git config --get-regex \n'^(some|section|names)\\.' -z\". I've found those to be effective in \nprograms that read several config keys. Elsewhere brian has mentioned \nthat git-lfs does something similar and I believe git-filter-repo uses \n\"git config --list -z\" as well.\n\nOne potential advantage would be if this command supported specifying \nthe type of the value. When using \"git config --list\" it is a pain to \nhave to normalize boolean values and parse color descriptions into \nterminal escape codes.\n\nBeing able to set multiple keys at once would also be an advantage if \nthere is a convincing use case for it.\n\n> This RFC adds git config-batch with a protocol over stdin/stdout for\n> executing multiple config queries. The implementation has a limited set of\n> potential queries, but also creates a model for compatibility for tools to\n> automatically adapt to different Git versions.\n> \n> I'm submitting this as an RFC before I've polished all of the details\n> because I want to make sure I'm going down a good direction. Please focus\n> feedback in these questions:\n> \n>   * Is this a worthwhile feature to add to Git?\n\nPossibly, if there are clear benefits over \"git config --list\". I'm not \nsure it needs to be a separate command though - I agree with Junio that \nit would be more discoverable if this was a subcommand of \"git config\"\n\n>   * Is this a reasonable protocol for stdin/stdout?\n\nThe protocol sounds quite complicated with capability queries and \nversioning. At the same time it looks like the line oriented version \ndoes not support keys that contain spaces, values that contain newlines \nor retrieving settings from a file whose path contains a newline. I \nthink it might be better to have a single protocol variant based using \nNUL delimiters like \"git merge-tree --stdin\" and \"git diff-pairs\". Those \ncommands use a simple NUL terminated protocol without the need to \nspecify the length of the input.\n\n> Each command has an associated version, in case we need to expand or alter\n> the functionality in the future. This includes the potential to deprecate\n> and remove certain versions that we no longer want to support, such as\n> replacing set version 1 with a version 2 and making version 1 no longer\n> available. I do hope that we will mostly be able to move with new command\n> names, such as a set-all command including the options for git config set\n> --all ... instead of increasing the version of the set command.\n\nIt's good that you're thinking about future functionality but I wonder \nif we really need to specify the version on a per command basis rather \nthan as a command line option or simply adding commands like \"get-v2\".\n\n> There is a -z option that changes the command interface to use\n> NUL-terminated strings. Two NULs specify a command boundary, which promotes\n> compatibility with a caller that sends an unknown command.\n\nThat also allows optional fields such as the file to read the config \nfrom or the value to match to be NUL delimited while making the end of a \nrecord unambiguous.\n\n> However, this\n> means that we cannot specify an empty string as a token within a command\n> unless we add more data. \n\nIs there a need to do that? Can we require key=value pairs or possibly a \nfixed number of positional parameters if there is a chance the value \nwill be empty?\n\n> This format uses <N>:<string> to provide the\n> integer <N> which specifies the length of <string>. This is a little\n> cumbersome, but the format is intended for tools, not humans.\n\nIt does seem cumbersome. I can see that it might be helpful to have the \nlength of the complete query and response to avoid deadlocks when \nreading and writing but I'm not sure requiring the length of each field \nis helpful.\n\n> I have a test integration with git-credential-manager available [1] for\n> testing. This includes a model for interacting with git config-batch in a\n> compatible way that will respond to certain features not being available:\n> \n>   1. If git config-batch fails immediately, then all queries are handled by\n>      git config.\n>   2. If git config-batch starts without failure, then the first query is for\n>      the help command.\n>   3. As queries come to the config system, the query is checked against the\n>      available commands advertised by git config-batch. If the appropriate\n>      command is available, then the query is made in that process. If not,\n>      then the query uses the existing git config command.\n\nThis seems like quite a lot of effort just to check a few config settings.\n\n> I have a few concerns with this implementation that I'd like to improve\n> before submitting a version for full review. I list them here so you can see\n> the flaws that I already see, but also so you can add to this list:\n> \n>   * The use of arg:<arg> to specify an optional argument creates the\n>     inability to submit a value that starts with arg:. Consider alternative\n>     ways to specify arguments or to specify that the remaining data in the\n>     command (including spaces) is a final positional argument.\n\nThe protocol should be unambiguous. Requiring key=value pairs for all \nfields would be one way to achieve that\n\n\tset key=my.key scope=global value-regex=my-regex value=new-value\n\nor we could force all optional fields to come first and count the number \nof fields to figure out whether any optional fields have been passed\n\n\tset value-regex=my-regex global my.key new-value\n\n(I've used spaces above to delimit fields but we'd want to use NUL in \nthe protocol)\n\nThanks\n\nPhillip\n\n>   * In general, I found myself implementing behavior based on the deprecated\n>     forms of git config that use the --get or --unset style arguments instead\n>     of git config (set|unset|get) subcommands. It's worth making sure that\n>     any references to equivalent git config commands use the new modes.\n>   * I need to add an --[no-]includes option as a command-line argument that\n>     signals whether include sections should be followed. I don't believe this\n>     should be specified on a per-command basis, but I'm open to suggestions.\n>   * I have an early draft of a technical document detailing the plan for this\n>     builtin. It has some lists of intended future commands that have not been\n>     implemented. This would also be a good place to document any parsing APIs\n>     built to help contributors adding to this builtin.\n> \n> Thanks, -Stolee\n> \n> Derrick Stolee (11):\n>    config-batch: basic boilerplate of new builtin\n>    config-batch: create parse loop and unknown command\n>    config-batch: implement get v1\n>    config-batch: create 'help' command\n>    config-batch: add NUL-terminated I/O format\n>    docs: add design doc for config-batch\n>    config: extract location structs from builtin\n>    config-batch: pass prefix through commands\n>    config-batch: add 'set' v1 command\n>    t1312: create read/write test\n>    config-batch: add unset v1 command\n> \n>   .gitignore                                |   1 +\n>   Documentation/git-config-batch.adoc       | 214 ++++++\n>   Documentation/meson.build                 |   1 +\n>   Documentation/technical/config-batch.adoc |  70 ++\n>   Makefile                                  |   1 +\n>   builtin.h                                 |   7 +\n>   builtin/config-batch.c                    | 772 ++++++++++++++++++++++\n>   builtin/config.c                          | 117 +---\n>   command-list.txt                          |   1 +\n>   config.c                                  | 116 ++++\n>   config.h                                  |  26 +\n>   git.c                                     |   1 +\n>   meson.build                               |   1 +\n>   t/meson.build                             |   1 +\n>   t/t1312-config-batch.sh                   | 372 +++++++++++\n>   15 files changed, 1592 insertions(+), 109 deletions(-)\n>   create mode 100644 Documentation/git-config-batch.adoc\n>   create mode 100644 Documentation/technical/config-batch.adoc\n>   create mode 100644 builtin/config-batch.c\n>   create mode 100755 t/t1312-config-batch.sh\n> \n> \n> base-commit: 83a69f19359e6d9bc980563caca38b2b5729808c\n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2033%2Fderrickstolee%2Fbatched-config-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2033/derrickstolee/batched-config-v1\n> Pull-Request: https://github.com/gitgitgadget/git/pull/2033\n\n"},{"id":"535259","messageId":"f4bc400d-183c-406f-9f8c-bfed973c2eb1@app.fastmail.com","threadId":"64916","inReplyTo":"pull.2033.git.1770214803.gitgitgadget@gmail.com","subject":"Re: [PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T17:20:17Z","receivedAt":"2026-02-05T17:20:38Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 4, 2026, at 15:19, Derrick Stolee via GitGitGadget wrote:\n> This RFC explores a new git config-batch builtin that allows tools to\n> interact with Git's config data with multiple queries using a single\n> process. This is an orthogonal alternative to the effort to create a stable,\n> linkable config API. Both approaches have different strengths.\n>[snip]\n\nThis sounds incredibly useful. Thanks!\n"},{"id":"535260","messageId":"9143e1ba-38f9-471c-a241-5505fe33bb99@app.fastmail.com","threadId":"64916","inReplyTo":"fdeef536f649bec811e8335d1c7151be8e352ff0.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 09/11] config-batch: add 'set' v1 command","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T17:21:53Z","receivedAt":"2026-02-05T17:22:14Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 4, 2026, at 15:19, Derrick Stolee via GitGitGadget wrote:\n>[snip]\n> +git-config-batch(1)\n> +===================\n> +\n> +NAME\n> +----\n> +git-config-batch - Get and set options using machine-parseable\n> interface\n> +\n> +\n> +SYNOPSIS\n> +--------\n> +[verse]\n\nThere’s work lead by Jean-Noël Avila to use `[synopsis]` instead of\n`[verse]`.[1] Would it make sense to start off with that?\n\n† 1: E.g. acffc5e9 (doc: convert git-remote to synopsis style, 2025-12-20)\n\n> +'git config-batch' <options>\n> +\n> +DESCRIPTION\n> +-----------\n>[snip]\n"},{"id":"535261","messageId":"a1144600-1c94-447f-beaf-8972cd9bdf0f@app.fastmail.com","threadId":"64916","inReplyTo":"6c8b984e-feda-48c6-b67d-80a41343bfc0@gmail.com","subject":"Re: [PATCH 01/11] config-batch: basic boilerplate of new builtin","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T17:26:28Z","receivedAt":"2026-02-05T17:26:49Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Thu, Feb 5, 2026, at 15:17, Derrick Stolee wrote:\n>>>[snip]\n>>\n>> We have had a bad reputation for having too many commands; would it\n>> be better to present it as a new mode of existing \"git config\"\n>> command at the end-user level, I wonder?\n>\n> Interesting thought. I think we also have a bad reputation of commands\n> that are overloaded with too many purposes.\n\nI had a response to that in my head...\n\n> In this case, though, I do think that the modern 'git config <subcommand>'\n> model presents some clear boundaries for how the command should behave\n> with the 'batch' (or 'server') subcommand. Grouping all config-related\n> operations in the same builtin may be ideal.\n\nWhich turned out to be exactly about a subcommand. :)\n\nI find the modern subcommand model very easy to navigate. And with much\nless downsides compared to having dozens of options for one command (or: one\nparticular subcommand to git(1)).\n\n>\n>> Also after reading patches for a few early steps, I do not quite see\n>> \"batch\"-ness in this protocol; it is strictly \"a single request is\n>> met with a single response\".\n>\n> The batch-ness is that multiple requests can eventually go to the same\n> process. The client could collect multiple commands in a batch and send\n> them all without processing the responses one-by-one. This is how it works\n> in the tests: a single input file is prepared and all responses are\n> scanned after-the-fact.\n\nAs a user that makes sense given the existing `--batch` and\n`--stdin` options.\n\n> The back-and-forth mechanism is how the git-credential-manager tool would\n> use it, because it dynamically explores certain config keys. For example:\n> it checks the deepest possible URL for a specific key then peels away the\n> last segment of the URL to see if there is a directory-prefix match in a\n> key. (This is the main reason that there are so many requests in this\n> application.)\n>\n> I believe this is similar to how 'git cat-file --batch' or 'git cat-file\n> --batch-check' work, which was my inspiration for this word. If we regret\n> those names, then I'm happy to move towards a better name.\n>\n> Thanks,\n> -Stolee\n"},{"id":"535262","messageId":"da3ae8d0-dda1-4b27-9e37-995dd8b89a1f@app.fastmail.com","threadId":"64916","inReplyTo":"c4dab0609613bc5d43bce705dca2f057674a5d5b.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 01/11] config-batch: basic boilerplate of new builtin","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T17:29:01Z","receivedAt":"2026-02-05T17:29:32Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 4, 2026, at 15:19, Derrick Stolee via GitGitGadget wrote:\n>[snip]\n> +git-config-batch(1)\n> +===================\n> +\n> +NAME\n> +----\n> +git-config-batch - Get and set options using machine-parseable\n> interface\n> +\n> +\n> +SYNOPSIS\n> +--------\n> +[verse]\n\nThere’s work lead by Jean-Noël Avila to use `[synopsis]` instead of\n`[verse]`.[1] Would it make sense to start off with that?\n\n† 1: E.g. acffc5e9 (doc: convert git-remote to synopsis style, 2025-12-20)\n\n> +'git config-batch' <options>\n> +\n> +DESCRIPTION\n> +-----------\n>[snip]\n"},{"id":"535263","messageId":"7c465b00-67c2-464b-b3db-d40685db7d2d@app.fastmail.com","threadId":"64916","inReplyTo":"ecd26a0f1fad5615aea07a388e34f02e9f33b870.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 02/11] config-batch: create parse loop and unknown command","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T17:30:06Z","receivedAt":"2026-02-05T17:30:27Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 4, 2026, at 15:19, Derrick Stolee via GitGitGadget wrote:\n>[snip]\n>  DESCRIPTION\n>  -----------\n> -TODO\n> +Tools frequently need to change their behavior based on values stored in\n> +Git's configuration files. These files may have complicated conditions\n> +for including extra files, so it is difficult to produce an independent\n> +parser. To avoid executing multiple processes to discover or modify\n> +multiple configuration values, the `git config-batch` command allows a\n> +single process to handle multiple requests using a machine-parseable\n> +interface across `stdin` and `stdout`.\n\nI really like that the doc itself motivates the command. Many man pages\non git(1) just tells you what it does as if you would already know why\nyou need it.\n\n> +\n>[snip]\n"},{"id":"535264","messageId":"bd2dbd12-e12f-467c-983b-f7e9a31e1d92@app.fastmail.com","threadId":"64916","inReplyTo":"59d19fee5f5bd34c5864bebb8243afdc6bc9ea7a.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 11/11] config-batch: add unset v1 command","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T17:36:33Z","receivedAt":"2026-02-05T17:36:54Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 4, 2026, at 15:20, Derrick Stolee via GitGitGadget wrote:\n> From: Derrick Stolee <stolee@gmail.com>\n>\n> Add a new 'unset' command with version 1 that mimics 'git config\n> --unset' with optional regex pattern or '--fixed-value' arguments.\n\n`git config --unset` is deprecated in favor of `git config unset`.\n\n>\n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  Documentation/git-config-batch.adoc | 28 ++++++++\n>  builtin/config-batch.c              | 99 +++++++++++++++++++++++++++++\n>  t/t1312-config-batch.sh             | 61 ++++++++++++++++--\n>  3 files changed, 181 insertions(+), 7 deletions(-)\n>\n> diff --git a/Documentation/git-config-batch.adoc\n> b/Documentation/git-config-batch.adoc\n> index feec85c4ef..bdfd872d65 100644\n> --- a/Documentation/git-config-batch.adoc\n> +++ b/Documentation/git-config-batch.adoc\n> @@ -135,6 +135,34 @@ set 1 success <scope> <key> <value>\n>  set 1 failed <scope> <key> <value>\n>  ------------\n>\n> +`unset` version 1::\n> +\tThe `unset` command removes a single value from a config file.\n> +\tIt specifies which file by a `<scope>` parameter from among\n> +\t`system`, `global`, `local`, and `worktree`. The `<key>` is the\n> +\tnext positional argument. There could be two additional\n> +\targuments used to match specific config values, where the first\n> +\tis either `arg:regex` or `arg:fixed-value` to specify the type\n> +\tof match.\n> ++\n> +------------\n> +unset 1 <scope> <key>\n> +unset 1 <scope> <key> arg:regex <value-pattern>\n> +unset 1 <scope> <key> arg:fixed-value <value>\n> +------------\n> ++\n> +These uses will match the behavior of `git config --unset --<scope> <key>`\n\nSame as above.\n\n> +with the additional arguments of `<value-pattern>` if `arg:regex` is\n> +given or `--fixed-value <value>` if `arg:fixed-value` is given.\n> ++\n> +The response of these commands will include a `success` message\n> +if matched values are found and removed as expected or `failed` if an\n> +unexpected failure occurs:\n> ++\n> +------------\n> +unset 1 success <scope> <key>\n> +unset 1 failed <scope> <key>\n> +------------\n> +\n>  NUL-Terminated Format\n>  ~~~~~~~~~~~~~~~~~~~~~\n>\n> diff --git a/builtin/config-batch.c b/builtin/config-batch.c\n> index 373b0cad47..25a942ba61 100644\n> --- a/builtin/config-batch.c\n> +++ b/builtin/config-batch.c\n> @@ -17,6 +17,7 @@ static int zformat = 0;\n>  #define HELP_COMMAND \"help\"\n>  #define GET_COMMAND \"get\"\n>  #define SET_COMMAND \"set\"\n> +#define UNSET_COMMAND \"unset\"\n>  #define COMMAND_PARSE_ERROR \"command_parse_error\"\n>\n>  static void print_word(const char *word, int start)\n> @@ -445,6 +446,99 @@ cleanup:\n>  \treturn res;\n>  }\n>\n> +/**\n> + * 'unset' command, version 1.\n> + *\n> + * Positional arguments should be of the form:\n> + *\n> + * [0] scope (\"system\", \"global\", \"local\", or \"worktree\")\n> + * [1] config key\n> + * [2] config value\n> + * [3*] match (\"regex\", \"fixed-value\")\n> + * [4*] value regex OR value string\n> + *\n> + * [N*] indicates optional parameters that are not needed.\n> + */\n> +static int unset_command_1(struct repository *repo,\n> +\t\t\t const char *prefix,\n> +\t\t\t char *data,\n> +\t\t\t size_t data_len)\n> +{\n> +\tint res = 0, err = 0, flags = 0;\n> +\tenum config_scope scope = CONFIG_SCOPE_UNKNOWN;\n> +\tchar *token = NULL, *key = NULL, *value_pattern = NULL;\n> +\tsize_t token_len;\n> +\tstruct config_location_options locopts = CONFIG_LOCATION_OPTIONS_INIT;\n> +\n> +\tif (!parse_token(&data, &data_len, &token, &err) || err)\n> +\t\tgoto parse_error;\n> +\n> +\tif (parse_scope(token, &scope) ||\n> +\t    scope == CONFIG_SCOPE_UNKNOWN ||\n> +\t    scope == CONFIG_SCOPE_SUBMODULE ||\n> +\t    scope == CONFIG_SCOPE_COMMAND)\n> +\t\tgoto parse_error;\n\nI think this should get braces since it has many lines? Or maybe\nmulti-line conditionals are excempt.\n\n> +\n> +\tif (!parse_token(&data, &data_len, &key, &err) || err)\n> +\t\tgoto parse_error;\n>[snip]\n"},{"id":"535265","messageId":"1702a6b0-78a0-49e8-b3e0-a112c251c9ed@app.fastmail.com","threadId":"64916","inReplyTo":"014e959cf4a4e19afe6becdb155f49d0f96739f8.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 06/11] docs: add design doc for config-batch","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T17:38:44Z","receivedAt":"2026-02-05T17:39:05Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 4, 2026, at 15:19, Derrick Stolee via GitGitGadget wrote:\n>[snip]\n> +Current commands\n> +----------------\n> +\n> +See the documentation in linkgit::config-batch[1] for the latest set of\n\ns/linkgit::config-batch[1]/linkgit:git-config-batch[1]/\n\n> +available commands and their protocols.\n> +\n> +Future commands\n> +---------------\n> +\n> +The following modes of `git config` are not currently available as\n> commands\n> +in `git config-batch`, but are planned for future integration:\n> +\n> +`git config list [--<scope>]`::\n> +\tGetting all values, regardless of config key, would require a\n> +\tmulti-valued output similar to the `help` command. This tool will\n> +\tlikely assume advanced options such as `--show-origin`.\n\nWhat does it mean to assume options?\n\n> +\n> +`git config set [--<scope>] <key> <value>`::\n> +\tIt will be desirable to set a config key at a given scope as a\n> +\tsingle value, replacing the current value at that scope, if it\n> +\texists and is a single value. A `set` command could satisfy this\n> +\tpurpose.\n> +\n> +`git config set --all [<value-pattern>|--fixed-value=<fixedvalue>]\n> <key> <value>`::\n> +\tWhen replacing multiple values, it may be necessary to have a\n> different\n> +\toutput describing the places those values were set, so it may need to\n> +\tbe implemented via a `set-all` command to differentiate from a `set`\n> +\tcommand.\n> +\n> +`git config unset <key>`::\n> +\n> +`git config unset --all [<value-pattern>|--fixed-value=<fixedvalue>]\n> <key>`::\n> +\n> +`git config get --all --rexexp <key-pattern> [<value-options>]`::\n> +\n> +`--replace-all` option::\n> +\n> +`--type=<type>` option::\n> --\n> gitgitgadget\n"},{"id":"535268","messageId":"e3f3fa17-fde7-45a0-8474-aa25290ff1bc@app.fastmail.com","threadId":"64916","inReplyTo":"33faa3f134c81761631c34600477dcbf82e619e5.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 05/11] config-batch: add NUL-terminated I/O format","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T17:44:23Z","receivedAt":"2026-02-05T17:44:44Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 4, 2026, at 15:19, Derrick Stolee via GitGitGadget wrote:\n> From: Derrick Stolee <stolee@gmail.com>\n>[snip]\n> +OPTIONS\n> +-------\n> +\n> +`-z`::\n> +\tIf specified, then use the NUL-terminated input and output\n\nIt seems to me that using the imperative mood for options might be\npreferred now. Like:\n\n    Use NUL-terminated input and output...\n\nSee: https://lore.kernel.org/git/bcd6fcd1190fe21c667b5253a4a33b833e658609.1769462744.git.gitgitgadget@gmail.com/\n\n>[snip]\n> -\tline provides the count of possible commands via `help count <N>`.\n> -\tThe next `<N>` lines are of the form `help <command> <version>`\n> +\tline provides the count of possible commands via `help 1 count <N>`.\n> +\tThe next `<N>` lines are of the form `help 1 <command> <version>`\n>  \tto state that this Git version supports that `<command>` at\n>  \tversion `<version>`. Note that the same command may have multiple\n>  \tavailable versions.\n>  +\n> -Here is the currentl output of the help text at the latest version:\n> +Here is the current output of the help text at the latest version:\n\nInnocent intra-series typofix.\n\n>  +\n>  ------------\n>  help 1 count 2\n> @@ -102,6 +111,48 @@ get 1 missing <key> [<value-pattern>|<value>]\n>  where `<value-pattern>` or `<value>` is only supplied if provided in\n>  the command.\n>\n> +NUL-Terminated Format\n> +~~~~~~~~~~~~~~~~~~~~~\n> +\n> +When `-z` is given, the protocol changes in some structural ways.\n\nIt might flow better with “Option `-z` changes the protocol...” ?\n\nI don’t know how usual it is to say “Option <x>”.\n\n>[snip]\n> +static void print_word(const char *word, int start)\n> +{\n> +\tif (zformat) {\n> +\t\tprintf(\"%\"PRIu32\":%s\", (uint32_t)strlen(word), word);\n> +\t\tfputc(0, stdout);\n> +\t} else if (start)\n\nAll of the arms should get braces here.\n\n> +\t\tprintf(\"%s\", word);\n> +\telse\n> +\t\tprintf(\" %s\", word);\n> +}\n> +\n>[snip]\n"},{"id":"535272","messageId":"826b18cf-bf75-4d60-9feb-c8e6662d2b6d@app.fastmail.com","threadId":"64916","inReplyTo":"9143e1ba-38f9-471c-a241-5505fe33bb99@app.fastmail.com","subject":"Re: [PATCH 09/11] config-batch: add 'set' v1 command","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T18:58:41Z","receivedAt":"2026-02-05T18:59:03Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Thu, Feb 5, 2026, at 18:21, Kristoffer Haugsbakk wrote:\n> On Wed, Feb 4, 2026, at 15:19, Derrick Stolee via GitGitGadget wrote:\n>>[snip]\n>> +git-config-batch(1)\n>> +===================\n>> +\n>> +NAME\n>> +----\n>> +git-config-batch - Get and set options using machine-parseable\n>> interface\n>> +\n>> +\n>> +SYNOPSIS\n>> +--------\n>> +[verse]\n>\n> There’s work lead by Jean-Noël Avila to use `[synopsis]` instead of\n> `[verse]`.[1] Would it make sense to start off with that?\n>\n> † 1: E.g. acffc5e9 (doc: convert git-remote to synopsis style, 2025-12-20)\n>\n>> +'git config-batch' <options>\n>> +\n>> +DESCRIPTION\n>> +-----------\n>>[snip]\n\n(sorry for replying to the wrong email)\n"},{"id":"535273","messageId":"1cb68e4f-930d-456d-ba1b-b153e7a66524@app.fastmail.com","threadId":"64916","inReplyTo":"fdeef536f649bec811e8335d1c7151be8e352ff0.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 09/11] config-batch: add 'set' v1 command","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-02-05T19:01:06Z","receivedAt":"2026-02-05T19:01:27Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Feb 4, 2026, at 15:20, Derrick Stolee via GitGitGadget wrote:\n>[snip]\n> +`set` version 1::\n> +\tThe `set` command writes a single key-value pair to a config\n> +\tfile. It specifies which file by a `<scope>` parameter from\n> +\tamong `system`, `global`, `local`, and `worktree`. The `<key>`\n> +\tis the next positional argument. The remaining data in the line\n> +\tis provided as the `<value>` to assign the config.\n> ++\n> +------------\n> +set 1 <scope> <key> <value>\n> +------------\n> ++\n> +These uses will match the behavior of `git config --set --<scope> <key>\n\n`--set` doesn’t exist. I think you meant `set`.\n\n>[snip]\n> +int location_options_set_scope(struct config_location_options *opts,\n> +\t\t\t       enum config_scope scope)\n> +{\n> +\tswitch (scope) {\n> +\tcase CONFIG_SCOPE_SYSTEM:\n> +\t\topts->use_system_config = 1;\n> +\t\tbreak;\n> +\n> +\tcase CONFIG_SCOPE_GLOBAL:\n> +\t\topts->use_global_config = 1;\n> +\t\tbreak;\n> +\n> +\tcase CONFIG_SCOPE_LOCAL:\n> +\t\topts->use_local_config = 1;\n> +\t\tbreak;\n> +\n> +\tcase CONFIG_SCOPE_WORKTREE:\n> +\t\topts->use_worktree_config = 1;\n> +\t\tbreak;\n> +\n> +\tdefault:\n> +\t\treturn -1;\n> +\t}\n\nIs there support for a user-provided file? (`git config --file=...`)\n\n> +\n> +\treturn 0;\n> +}\n> +\n>[snip]\n"},{"id":"535297","messageId":"f1f65415-7a50-451e-9826-05e9d4e38b62@free.fr","threadId":"64916","inReplyTo":"c4dab0609613bc5d43bce705dca2f057674a5d5b.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 01/11] config-batch: basic boilerplate of new builtin","fromName":"Jean-Noël Avila","fromEmail":"jn.avila@free.fr","sentAt":"2026-02-06T04:11:02Z","receivedAt":"2026-02-06T04:11:11Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Le 04/02/2026 à 15:19, Derrick Stolee via GitGitGadget a écrit :\n> From: Derrick Stolee <stolee@gmail.com>\n> \n> Later changes will document, implement, and test this new builtin. For now,\n> this serves as the latest example of the minimum boilerplate to introduce a\n> new builtin.\n> \n> Recently, we updated the comment in builtin.h about how to create a new\n> builtin, but failed to mention the required change to meson.build files for\n> some CI builds to pass. Fix that oversight.\n> \n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  .gitignore                          |  1 +\n>  Documentation/git-config-batch.adoc | 24 +++++++++++++++++++++++\n>  Documentation/meson.build           |  1 +\n>  Makefile                            |  1 +\n>  builtin.h                           |  7 +++++++\n>  builtin/config-batch.c              | 30 +++++++++++++++++++++++++++++\n>  command-list.txt                    |  1 +\n>  git.c                               |  1 +\n>  meson.build                         |  1 +\n>  t/meson.build                       |  1 +\n>  t/t1312-config-batch.sh             | 12 ++++++++++++\n>  11 files changed, 80 insertions(+)\n>  create mode 100644 Documentation/git-config-batch.adoc\n>  create mode 100644 builtin/config-batch.c\n>  create mode 100755 t/t1312-config-batch.sh\n> \n> diff --git a/.gitignore b/.gitignore\n> index 78a45cb5be..42640b5e24 100644\n> --- a/.gitignore\n> +++ b/.gitignore\n> @@ -44,6 +44,7 @@\n>  /git-commit-graph\n>  /git-commit-tree\n>  /git-config\n> +/git-config-batch\n>  /git-count-objects\n>  /git-credential\n>  /git-credential-cache\n> diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\n> new file mode 100644\n> index 0000000000..dfa0bd83e2\n> --- /dev/null\n> +++ b/Documentation/git-config-batch.adoc\n> @@ -0,0 +1,24 @@\n> +git-config-batch(1)\n> +===================\n> +\n> +NAME\n> +----\n> +git-config-batch - Get and set options using machine-parseable interface\n> +\n> +\n> +SYNOPSIS\n> +--------\n> +[verse]\n> +'git config-batch' <options>\n\nFor this new manual page, please use the synopsis style:\n\n[synopsis]\ngit config-batch <options>\n\nThanks\n"},{"id":"535303","messageId":"f6f8c84b-4672-49ac-bda3-0205cdeaff9c@free.fr","threadId":"64916","inReplyTo":"ecd26a0f1fad5615aea07a388e34f02e9f33b870.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 02/11] config-batch: create parse loop and unknown command","fromName":"Jean-Noël Avila","fromEmail":"jn.avila@free.fr","sentAt":"2026-02-06T04:15:49Z","receivedAt":"2026-02-06T04:15:57Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Le 04/02/2026 à 15:19, Derrick Stolee via GitGitGadget a écrit :\n> From: Derrick Stolee <stolee@gmail.com>\n> \n> As we build new features in the config-batch command, we define the\n> plaintext protocol with line-by-line output and responses. To think to the\n> future, we make sure that the protocol has a clear way to respond to an\n> unknown command or an unknown version of that command.\n> \n> As some commands will allow the final argument to contain spaces or even be\n> able to parse \"\\ \" as a non-split token, we only provide the remaining line\n> as data.\n> \n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  Documentation/git-config-batch.adoc |  23 ++++-\n>  builtin/config-batch.c              | 133 +++++++++++++++++++++++++++-\n>  t/t1312-config-batch.sh             |  19 +++-\n>  3 files changed, 170 insertions(+), 5 deletions(-)\n> \n> diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\n> index dfa0bd83e2..9ca04b0c1e 100644\n> --- a/Documentation/git-config-batch.adoc\n> +++ b/Documentation/git-config-batch.adoc\n> @@ -13,7 +13,28 @@ SYNOPSIS\n>  \n>  DESCRIPTION\n>  -----------\n> -TODO\n> +Tools frequently need to change their behavior based on values stored in\n> +Git's configuration files. These files may have complicated conditions\n> +for including extra files, so it is difficult to produce an independent\n> +parser. To avoid executing multiple processes to discover or modify\n> +multiple configuration values, the `git config-batch` command allows a\n> +single process to handle multiple requests using a machine-parseable\n> +interface across `stdin` and `stdout`.\n> +\n> +PROTOCOL\n> +--------\n> +By default, the protocol uses line feeds (`LF`) to signal the end of a\n\nCharacters are typefaced as placeholders: _LF_\n\n> +command over `stdin` or a response over `stdout`.\n> +\n> +The protocol will be extended in the future, and consumers should be\n> +resilient to older Git versions not understanding the latest command\n> +set. Thus, if the Git version includes the `git config-batch` builtin\n> +but doesn't understand an input command, it will return a single line\n> +response:\n> +\n> +```\n> +unknown_command LF> +```\n>  \nThis is Markdown. For Asciidoc, use code block:\n\n----\nunknown_command LF\n----\n\n\n\n\n\n"},{"id":"535304","messageId":"61926b22-dca1-4e5d-a911-6fc47dee68d6@free.fr","threadId":"64916","inReplyTo":"3de1bba3b10668f0200e27def9128571f51c1f68.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 03/11] config-batch: implement get v1","fromName":"Jean-Noël Avila","fromEmail":"jn.avila@free.fr","sentAt":"2026-02-06T04:41:49Z","receivedAt":"2026-02-06T04:41:57Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Le 04/02/2026 à 15:19, Derrick Stolee via GitGitGadget a écrit :\n> From: Derrick Stolee <stolee@gmail.com>\n> \n> The 'get' command for the 'git config-batch' builtin is the first command\n> and is currently at version 1. It returns at most one value, the same as\n> 'git config --get <key>' with optional value-based filtering.\n> \n> The documentation and tests detail the specifics of how to format requests\n> of this format and how to parse the results.\n> \n> Future versions could consider multi-valued responses or regex-based key\n> matching.\n> \n> For the sake of incremental exploration of the potential in the 'git\n> config-batch' command, this is the only implementation being presented in\n> the first patch series.\n> \n> Future extensions could include a '-z' parameter that uses NUL bytes in the\n> command and output format to allow for spaces or newlines in the input or\n> newlines in the output.\n> \n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  Documentation/git-config-batch.adoc |  53 +++++-\n>  builtin/config-batch.c              | 251 +++++++++++++++++++++++++++-\n>  config.h                            |   3 +\n>  t/t1312-config-batch.sh             | 101 +++++++++++\n>  4 files changed, 405 insertions(+), 3 deletions(-)\n> \n> diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\n> index 9ca04b0c1e..31dd42f481 100644\n> --- a/Documentation/git-config-batch.adoc\n> +++ b/Documentation/git-config-batch.adoc\n> @@ -32,9 +32,58 @@ set. Thus, if the Git version includes the `git config-batch` builtin\n>  but doesn't understand an input command, it will return a single line\n>  response:\n>  \n> -```\n> +------------\n>  unknown_command LF\n> -```\n> +------------\n> +\n\nOK, the change to Asciidoc code block is done here. Would it be possible\nto push it up at the introduction of these lines?\n\n> +These are the commands that are currently understood:\n> +\n> +`get` version 1::\n> +\tThe `get` command searches the config key-value pairs within a\n> +\tgiven `<scope>` for values that match the fixed `<key>` and\n\nThe rendering of these is correct due to the synopsis formatter, but we\nusually prefer to use the direct formatting for placeholders: _<scope>_,\n_<key>_,…\n\n> +\tfilters the resulting value based on an optional `<value-filter>`.\n> +\tThis can either be a regex or a fixed value. The command format\n> +\tis one of the following formats:\n> ++\n> +------------\n> +get 1 <scope> <key>\n> +get 1 <scope> <key> arg:regex <value-pattern>\n> +get 1 <scope> <key> arg:fixed-value <value>\n> +------------\n> ++\n\nIf you are using synopsis style in the block, with the upcoming change\nof synopsis style block[1], you can format it:\n\n[synopsis]\n------------\nget 1 <scope> <key>\nget 1 <scope> <key> arg:regex <value-pattern>\nget 1 <scope> <key> arg:fixed-value <value>\n------------\n\n> +The `<scope>` value can be one of `inherited`, `system`, `global`,\n> +`local`, `worktree`, `submodule`, or `command`. If `inherited`, then all\n> +config key-value pairs will be considered regardless of scope. Otherwise,\n> +only the given scope will be considered.\n> ++\n> +If no optional arguments are given, then the value will not be filtered\n> +by any pattern matching. If `arg:regex` is specified, then the rest of\n> +the line is considered a single string, `<value-pattern>`, and is\n> +interpreted as a regular expression for matching against stored values,\n> +similar to specifying a value to `get config --get <key> \"<value-pattern>\"`.\n> +If `arg:fixed-value` is specified, then the rest of the line is\n> +considered a single string, `<value>`, and is checked for an exact\n> +match against the key-value pairs, simmilar to `git config --get <key>\n\nsimilar\n\n> +--fixed-value \"<value>\"`.\n> ++\n\nHere I would use a sub definition list for each matching type, instead\nof long running description paragraph.\n\noptional arguments can be specified:\n\nno optional arguments;;\nthe value will not be filteredby any pattern matching.\n`arg:regex <value-pattern>`;;\n`<value-pattern>` is interpreted as a regular expression for matching\nagainst stored values, similar to specifying a value to `get config\n--get <key> \"<value-pattern>\"`.\n`arg:fixed-value <value>`;;\n`<value>` is checked for an exact match against the key-value pairs,\nsimilar to `git config --get <key>`.\n\n> +At mmost one key-value pair is returned, that being the last key-value\n\nAt most\n\n> +pair in the standard config order by scope and sequence within each scope.\n> ++\n> +If a key-value pair is found, then the following output is given:\n> ++\n> +------------\n> +get 1 found <key> <scope> <value>\n> +------------\n> ++\n> +If no matching key-value pair is found, then the following output is\n> +given:\n> ++\n> +------------\n> +get 1 missing <key> [<value-pattern>|<value>]\n> +------------\n> ++\n\nPlease also apply synopsis block style.\n\n> +where `<value-pattern>` or `<value>` is only supplied if provided in\n> +the command.\n>  \n>  SEE ALSO\n>  --------\n\n[1]:\nhttps://lore.kernel.org/git/6a2b94e720862fa07fe9463ebf7f7beaa9a1ccd4.1770351146.git.gitgitgadget@gmail.com/T/#u\n"},{"id":"535305","messageId":"a023e4a2-e58f-49c7-83ee-a84554b83bc6@free.fr","threadId":"64916","inReplyTo":"d5e0c32497581e6ac4890c6e71c5c33b92d67d51.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 04/11] config-batch: create 'help' command","fromName":"Jean-Noël Avila","fromEmail":"jn.avila@free.fr","sentAt":"2026-02-06T04:49:09Z","receivedAt":"2026-02-06T04:49:17Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Le 04/02/2026 à 15:19, Derrick Stolee via GitGitGadget a écrit :\n> From: Derrick Stolee <stolee@gmail.com>\n> \n> Tools that use the 'git config-batch' tool will want to know which commands\n> are available in the current Git version. Having a 'help' command assists\n> greatly to give a clear set of available commands and their versions.\n> \n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  Documentation/git-config-batch.adoc | 17 +++++++++++++++\n>  builtin/config-batch.c              | 32 +++++++++++++++++++++++++++++\n>  t/t1312-config-batch.sh             | 13 ++++++++++++\n>  3 files changed, 62 insertions(+)\n> \n> diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\n> index 31dd42f481..1fff68a13c 100644\n> --- a/Documentation/git-config-batch.adoc\n> +++ b/Documentation/git-config-batch.adoc\n> @@ -38,6 +38,23 @@ unknown_command LF\n>  \n>  These are the commands that are currently understood:\n>  \n> +`help` version 1::\n> +\tThe `help` command lists the currently-available commands in\n\nThe boilerplat text \"The `help` command\" is not very useful to the\nreader. The new usage is to directly state the command in imperative mood:\n\nList the currently...\n\n> +\tthis version of Git. The output is multi-line, but the first\n> +\tline provides the count of possible commands via `help count <N>`.\n> +\tThe next `<N>` lines are of the form `help <command> <version>`\n> +\tto state that this Git version supports that `<command>` at\n> +\tversion `<version>`. Note that the same command may have multiple\n> +\tavailable versions.\n\nPlaceholder punning to keep a consistency between the command and its\ndescription. Good!\n\n> ++\n> +Here is the currentl output of the help text at the latest version:\n\ncurrent\n\nIt may not be wise to talk about the \"latest version\". If the manpages\nand the git command are out of sync (the user compiles her own git\nversion, but does not update the man pages), this may be confusing.\n\nIs this specification of version critical to the understanding?\n\n\n> ++\n> +------------\n> +help 1 count 2\n> +help 1 help 1\n> +help 1 get 1\n> +------------\n> +\n>  `get` version 1::\n>  \tThe `get` command searches the config key-value pairs within a\n>  \tgiven `<scope>` for values that match the fixed `<key>` and\n"},{"id":"535306","messageId":"7204ff93-79d3-44f6-989d-184f00b86a2a@free.fr","threadId":"64916","inReplyTo":"33faa3f134c81761631c34600477dcbf82e619e5.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 05/11] config-batch: add NUL-terminated I/O format","fromName":"Jean-Noël Avila","fromEmail":"jn.avila@free.fr","sentAt":"2026-02-06T04:58:21Z","receivedAt":"2026-02-06T04:58:29Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Le 04/02/2026 à 15:19, Derrick Stolee via GitGitGadget a écrit :\n> From: Derrick Stolee <stolee@gmail.com>\n> \n> When using automated tools, it is critical to allow for input/output formats\n> that include special characters such as spaces and newlines. While the\n> existing protocol for 'git config-batch' is human-readable and has some\n> capacity for some spaces in certain positions, it is not available for\n> spaces in the config key or newlines in the config values.\n> \n> Add the '-z' option to signal the use of NUL-terminated strings. To\n> understand where commands end regardless of potential future formats, use\n> two NUL bytes in a row to terminate a command. To allow for empty string\n> values, each token is provided in a <length>:<value> format, making \"0:\"\n> the empty string value.\n> \n> Update the existing 'help' and 'get' commands to match this format. Create\n> helper methods that make it easy to parse and print in both formats\n> simultaneously.\n> \n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  Documentation/git-config-batch.adoc |  57 ++++++++-\n>  builtin/config-batch.c              | 188 +++++++++++++++++++++++++---\n>  t/t1312-config-batch.sh             |  69 ++++++++++\n>  3 files changed, 293 insertions(+), 21 deletions(-)\n> \n> diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\n> index 1fff68a13c..3c9a3bb763 100644\n> --- a/Documentation/git-config-batch.adoc\n> +++ b/Documentation/git-config-batch.adoc\n> @@ -21,6 +21,15 @@ multiple configuration values, the `git config-batch` command allows a\n>  single process to handle multiple requests using a machine-parseable\n>  interface across `stdin` and `stdout`.\n>  \n> +OPTIONS\n> +-------\n> +\n> +`-z`::\n> +\tIf specified, then use the NUL-terminated input and output\n\nThis boilerplate preliminary does not convey information, it is simpler\nto just jump to the action performed by the option:\n\nUse the _NUL_-terminated input and output…\n\n> +\tformat instead of the space and newline format. This format is\n> +\tuseful when the strings involved may include spaces or newlines.\n> +\tSee PROTOCOL for more details.\n> +\n>  PROTOCOL\n>  --------\n>  By default, the protocol uses line feeds (`LF`) to signal the end of a\n> @@ -41,13 +50,13 @@ These are the commands that are currently understood:\n>  `help` version 1::\n>  \tThe `help` command lists the currently-available commands in\n>  \tthis version of Git. The output is multi-line, but the first\n> -\tline provides the count of possible commands via `help count <N>`.\n> -\tThe next `<N>` lines are of the form `help <command> <version>`\n> +\tline provides the count of possible commands via `help 1 count <N>`.\n> +\tThe next `<N>` lines are of the form `help 1 <command> <version>`\n>  \tto state that this Git version supports that `<command>` at\n>  \tversion `<version>`. Note that the same command may have multiple\n>  \tavailable versions.\n>  +\n> -Here is the currentl output of the help text at the latest version:\n> +Here is the current output of the help text at the latest version:\n\nOK, the typo was fixed here.\n\n>  +\n>  ------------\n>  help 1 count 2\n> @@ -102,6 +111,48 @@ get 1 missing <key> [<value-pattern>|<value>]\n>  where `<value-pattern>` or `<value>` is only supplied if provided in\n>  the command.\n>  \n> +NUL-Terminated Format\n> +~~~~~~~~~~~~~~~~~~~~~\n> +\n> +When `-z` is given, the protocol changes in some structural ways.\n> +\n> +First, each command is terminated with two NUL bytes, providing a clear\n> +boundary between commands regardless of future possibilities of new\n> +command formats.\n> +\n> +Second, any time that a space _would_ be used to partition tokens in a\n> +command, a NUL byte is used instead. Further, each token is prefixed\n> +with `<N>:` where `<N>` is a decimal representation of the length of\n> +the string between the `:` and the next NUL byte. Any disagreement in\n> +these lengths is treated as a parsing error. This use of a length does\n\nI thought this length encoding was used to allow _NUL_ in the config\nvalues. But here it is considered a parse error.\n\n> +imply that \"`0:`\" is the representation of an empty string, if relevant.\n> +\n> +The decimal representation must have at most five numerals, thus the\n> +maximum length of a string token can have 99999 characters.\n> +\n> +For example, the `get` command, version 1, could have any of the\n> +following forms:\n> +\n> +------------\n> +3:get NUL 1:1 NUL 5:local NUL 14:key.with space NUL NUL\n> +3:get NUL 1:1 NUL 9:inherit NUL 8:test.key NUL 9:arg:regex NUL 6:.*\\ .* NUL NUL\n> +3:get NUL 1:1 NUL 6:global NUL 8:test.key NUL 15:arg:fixed-value NUL 3:a b NUL NUL\n> +------------\n> +\n> +The output is modified similarly, such as the following output examples,\n> +as if the input has a parse error, a valid `help` command, a `get`\n> +command that had a match, and a `get` command that did not match.\n> +\n> +------------\n> +15:unknown_command NUL NUL\n> +4:help NUL 1:1 NUL 5:count NUL 1:2 NUL NUL\n> +4:help NUL 1:1 NUL 4:help NUL 1:1 NUL NUL\n> +4:help NUL 1:1 NUL 3:get NUL 1:1 NUL NUL\n> +3:get NUL 1:1 NUL 5:found NUL 8:test.key NUL 5:value NUL NUL\n> +3:get NUL 1:1 NUL 7:missing NUL 8:test.key NUL NUL\n> +------------\n> +\n> +\n>  SEE ALSO\n>  --------\n>  linkgit:git-config[1]\n"},{"id":"535307","messageId":"5861406e-0ac7-4b96-9bde-cd3860ad5c4d@free.fr","threadId":"64916","inReplyTo":"fdeef536f649bec811e8335d1c7151be8e352ff0.1770214803.git.gitgitgadget@gmail.com","subject":"Re: [PATCH 09/11] config-batch: add 'set' v1 command","fromName":"Jean-Noël Avila","fromEmail":"jn.avila@free.fr","sentAt":"2026-02-06T05:04:59Z","receivedAt":"2026-02-06T05:05:07Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"Le 04/02/2026 à 15:20, Derrick Stolee via GitGitGadget a écrit :\n> From: Derrick Stolee <stolee@gmail.com>\n> \n> This new command is intended for single-value assignments to a specific\n> chosen scope. More complicated versions of the 'git config set' command\n> will be incorporated into future commands.\n> \n> Signed-off-by: Derrick Stolee <stolee@gmail.com>\n> ---\n>  Documentation/git-config-batch.adoc | 24 ++++++++\n>  builtin/config-batch.c              | 71 ++++++++++++++++++++++\n>  config.c                            | 27 +++++++++\n>  config.h                            |  3 +\n>  t/t1312-config-batch.sh             | 94 ++++++++++++++++++++++++++++-\n>  5 files changed, 217 insertions(+), 2 deletions(-)\n> \n> diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc\n> index 3c9a3bb763..feec85c4ef 100644\n> --- a/Documentation/git-config-batch.adoc\n> +++ b/Documentation/git-config-batch.adoc\n> @@ -111,6 +111,30 @@ get 1 missing <key> [<value-pattern>|<value>]\n>  where `<value-pattern>` or `<value>` is only supplied if provided in\n>  the command.\n>  \n> +`set` version 1::\n> +\tThe `set` command writes a single key-value pair to a config\n\nPlease use direct imperative form.\n\n> +\tfile. It specifies which file by a `<scope>` parameter from\n> +\tamong `system`, `global`, `local`, and `worktree`. The `<key>`\n> +\tis the next positional argument. The remaining data in the line\n> +\tis provided as the `<value>` to assign the config.\n> ++\n> +------------\n> +set 1 <scope> <key> <value>\n> +------------\n> ++\n> +These uses will match the behavior of `git config --set --<scope> <key>\n\nThis \"--<scope>\" form is new in the synopsis grammar. Would we just cite\nall alternatives or use a \"normal\" placeholder _<scope>_ ?\n\n> +<value>`. Note that replacing all values with the `--all` option or\n> +matching specific value patterns are not supported by this command.\n> ++\n> +The response of these commands will include a `success` message if the\n> +value is written as expected or `failed` if an unexpected failure\n> +occurs:\n> ++\n> +------------\n> +set 1 success <scope> <key> <value>\n> +set 1 failed <scope> <key> <value>\n> +------------\n> +\n\nPlease use synopsis style block for these too.\n\n>  NUL-Terminated Format\n>  ~~~~~~~~~~~~~~~~~~~~~\n>  \n"},{"id":"535636","messageId":"d3a49329-ce24-406e-9f33-10c623f40df6@gmail.com","threadId":"64916","inReplyTo":"a023e4a2-e58f-49c7-83ee-a84554b83bc6@free.fr","subject":"Re: [PATCH 04/11] config-batch: create 'help' command","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-02-10T04:20:25Z","receivedAt":"2026-02-10T04:20:27Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 2/5/2026 11:49 PM, Jean-Noël Avila wrote:\n> Le 04/02/2026 à 15:19, Derrick Stolee via GitGitGadget a écrit :\n\n>> ++\n>> +Here is the currentl output of the help text at the latest version:\n> It may not be wise to talk about the \"latest version\". If the manpages\n> and the git command are out of sync (the user compiles her own git\n> version, but does not update the man pages), this may be confusing.\n> \n> Is this specification of version critical to the understanding?\n\nIt's important to talk about how to build tools that work against\nversions that don't match the current documentation.\n\nIf you build something against v2.58.0 and we deprecate them in\nv2.59.0 and delete them in v3.0.0, then the tool should know that\nthe command isn't available (and maybe it was replaced with a v2).\n\nThe same holds for someone who builds against v3.0.0 but their\ntool is run against v2.58.0 and the feature they want to use isn't\navailable (or not at the same version).\n\nThanks,\n-Stolee\n\n"},{"id":"535637","messageId":"a9c39434-c179-491f-87fb-52b1c2705790@gmail.com","threadId":"64916","inReplyTo":"1702a6b0-78a0-49e8-b3e0-a112c251c9ed@app.fastmail.com","subject":"Re: [PATCH 06/11] docs: add design doc for config-batch","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-02-10T04:22:12Z","receivedAt":"2026-02-10T04:22:14Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 2/5/2026 12:38 PM, Kristoffer Haugsbakk wrote:\n> On Wed, Feb 4, 2026, at 15:19, Derrick Stolee via GitGitGadget wrote:\n\n>> +`git config list [--<scope>]`::\n>> +\tGetting all values, regardless of config key, would require a\n>> +\tmulti-valued output similar to the `help` command. This tool will\n>> +\tlikely assume advanced options such as `--show-origin`.\n> \n> What does it mean to assume options?\n\nI mean that since we expect to have the output parsed by tools, then\nwe will probably want the maximum amount of information by default.\nMaybe --show-origin isn't as helpful as --show-scope.\n \nThanks,\n-Stolee\n\n"},{"id":"535638","messageId":"3455bd60-abe4-429b-b684-340a713d0b13@gmail.com","threadId":"64916","inReplyTo":"1cb68e4f-930d-456d-ba1b-b153e7a66524@app.fastmail.com","subject":"Re: [PATCH 09/11] config-batch: add 'set' v1 command","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-02-10T04:25:03Z","receivedAt":"2026-02-10T04:25:05Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 2/5/2026 2:01 PM, Kristoffer Haugsbakk wrote:\n> On Wed, Feb 4, 2026, at 15:20, Derrick Stolee via GitGitGadget wrote:\n>> [snip]\n>> +`set` version 1::\n>> +\tThe `set` command writes a single key-value pair to a config\n>> +\tfile. It specifies which file by a `<scope>` parameter from\n>> +\tamong `system`, `global`, `local`, and `worktree`. The `<key>`\n>> +\tis the next positional argument. The remaining data in the line\n>> +\tis provided as the `<value>` to assign the config.\n>> ++\n>> +------------\n>> +set 1 <scope> <key> <value>\n>> +------------\n>> ++\n>> +These uses will match the behavior of `git config --set --<scope> <key>\n> \n> `--set` doesn’t exist. I think you meant `set`.\n\nYou're right. Also `git config --<scope> <key>` is the older mode. I'm\nnot always catching myself using the old format. Or inventing a mixed-up\none that never existed!\n\n> Is there support for a user-provided file? (`git config --file=...`)\n\nNot at the moment. It's worth thinking about what that interface would\nbe within this query model.\n\nMy initial feeling is that we wouldn't want to accept arbitrary filenames\non a per-command basis, but instead would want to provide an alternate\nfile in the command-line arguments as a replacement for the local config.\n\nThanks,\n-Stolee\n\n"},{"id":"535646","messageId":"34eee0c0-48e3-45ac-b187-d21580ac4c65@gmail.com","threadId":"64916","inReplyTo":"f6687192-58dd-479e-8df5-a422c01f03f4@gmail.com","subject":"Re: [PATCH 00/11] [RFC] config-batch: a new builtin for tools querying config","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2026-02-10T04:49:11Z","receivedAt":"2026-02-10T04:49:14Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 2/5/2026 8:52 AM, Derrick Stolee wrote:\n> On 2/4/2026 7:04 PM, brian m. carlson wrote:\n>> On 2026-02-04 at 14:19:52, Derrick Stolee via GitGitGadget wrote:\n\n>>>  * Is this a worthwhile feature to add to Git?\n>>\n>> Git LFS has the same needs, but I believe it can use `git config -l -z`\n>> to do that and parse the config options itself.  If this is just config\n>> fetching, I'm not sure of the additional utility that such a feature\n>> would add.  If that interface _almost_ meets your needs, could we add\n>> functionality there instead of a new interface?\n> \n> This is a good suggestion to look into as a potentially-easier solution.\nAfter digging into this, I realized that GCM uses Git's --type=<X>\noption, which doesn't work with 'git config list'!\n\nPlease see a new RFC [1] that adds that feature, though it is a\n\"breaking\" change from previous behavior.\n\n[1] https://lore.kernel.org/git/pull.2044.git.1770698579.gitgitgadget@gmail.com/\n\nThere's still some awkwardness in my GCM prototype, as it can\nrequire three commands to query all the types (no type, path, and\nbook) that are needed. I found that the slowest queries are using\nthe path type, but only because they are the most frequent ones.\n\nThis awkwardness does make me think both of these things:\n\n1. I can get performance boosts to GCM faster by the RFC in [1].\n\n2. Using 'git config list' isn't sufficient to minimize multiple\n   processes.\n\nFor now, I'll put _this_ RFC down for a little while to pursue\nthose easier gains. I'll come back again and consider all of the\nbig-picture considerations, including:\n\n* Make this a subcommand of 'git config'.\n\n* Make this a server that can serve multiple client processes.\n\n* Ensure that all \"complicated\" options are accounted for.\n\nThanks,\n-Stolee\n\n"}]}