From 2cbdd9cb744990160d8b3f8e36a2f6cfe622e351 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Thu, 8 Jan 2026 17:02:24 -0500 Subject: [PATCH 1/4] config-batch: basic boilerplate of new builtin Later changes will document, implement, and test this new builtin. For now, this serves as the latest example of the minimum boilerplate to introduce a new builtin. Recently, we updated the comment in builtin.h about how to create a new builtin, but failed to mention the required change to meson.build for some CI builds to pass. Fix that oversight. Signed-off-by: Derrick Stolee --- .gitignore | 1 + Documentation/git-config-batch.adoc | 24 +++++++++++++++++++++++ Makefile | 1 + builtin.h | 5 +++++ builtin/config-batch.c | 30 +++++++++++++++++++++++++++++ command-list.txt | 1 + git.c | 1 + meson.build | 1 + t/meson.build | 1 + t/t1312-config-batch.sh | 12 ++++++++++++ 10 files changed, 77 insertions(+) create mode 100644 Documentation/git-config-batch.adoc create mode 100644 builtin/config-batch.c create mode 100755 t/t1312-config-batch.sh diff --git a/.gitignore b/.gitignore index 78a45cb5bec991..42640b5e249c8f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ /git-commit-graph /git-commit-tree /git-config +/git-config-batch /git-count-objects /git-credential /git-credential-cache diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc new file mode 100644 index 00000000000000..dfa0bd83e25f61 --- /dev/null +++ b/Documentation/git-config-batch.adoc @@ -0,0 +1,24 @@ +git-config-batch(1) +=================== + +NAME +---- +git-config-batch - Get and set options using machine-parseable interface + + +SYNOPSIS +-------- +[verse] +'git config-batch' + +DESCRIPTION +----------- +TODO + +SEE ALSO +-------- +linkgit:git-config[1] + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Makefile b/Makefile index b7eba509c6a0ca..d31778169ac5dd 100644 --- a/Makefile +++ b/Makefile @@ -1390,6 +1390,7 @@ BUILTIN_OBJS += builtin/commit-graph.o BUILTIN_OBJS += builtin/commit-tree.o BUILTIN_OBJS += builtin/commit.o BUILTIN_OBJS += builtin/config.o +BUILTIN_OBJS += builtin/config-batch.o BUILTIN_OBJS += builtin/count-objects.o BUILTIN_OBJS += builtin/credential-cache--daemon.o BUILTIN_OBJS += builtin/credential-cache.o diff --git a/builtin.h b/builtin.h index e5e16ecaa6c9d7..f3c6a107608139 100644 --- a/builtin.h +++ b/builtin.h @@ -68,10 +68,14 @@ * * . Add `builtin/foo.o` to `BUILTIN_OBJS` in `Makefile`. * + * . Add 'builtin/foo.c' to the 'builtin_sources' array in 'meson.build'. + * * Additionally, if `foo` is a new command, there are 4 more things to do: * * . Add tests to `t/` directory. * + * . Add the test script to 'integration_tests' in 't/meson.build'. + * * . Write documentation in `Documentation/git-foo.adoc`. * * . Add an entry for `git-foo` to `command-list.txt`. @@ -167,6 +171,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix, struct repositor int cmd_commit_graph(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_commit_tree(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_config(int argc, const char **argv, const char *prefix, struct repository *repo); +int cmd_config_batch(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_count_objects(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_credential(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_credential_cache(int argc, const char **argv, const char *prefix, struct repository *repo); diff --git a/builtin/config-batch.c b/builtin/config-batch.c new file mode 100644 index 00000000000000..ea4f408ecb45d6 --- /dev/null +++ b/builtin/config-batch.c @@ -0,0 +1,30 @@ +#define USE_THE_REPOSITORY_VARIABLE +#include "builtin.h" +#include "config.h" +#include "environment.h" +#include "parse-options.h" + +static const char *const builtin_config_batch_usage[] = { + N_("git config-batch "), + NULL +}; + +int cmd_config_batch(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + struct option options[] = { + OPT_END(), + }; + + show_usage_with_options_if_asked(argc, argv, + builtin_config_batch_usage, options); + + argc = parse_options(argc, argv, prefix, options, builtin_config_batch_usage, + 0); + + repo_config(repo, git_default_config, NULL); + + return 0; +} diff --git a/command-list.txt b/command-list.txt index accd3d0c4b5524..57c7c7458d9b26 100644 --- a/command-list.txt +++ b/command-list.txt @@ -83,6 +83,7 @@ git-commit mainporcelain history git-commit-graph plumbingmanipulators git-commit-tree plumbingmanipulators git-config ancillarymanipulators complete +git-config-batch plumbinginterrogators git-count-objects ancillaryinterrogators git-credential purehelpers git-credential-cache purehelpers diff --git a/git.c b/git.c index c5fad56813f437..6b55a867dd5809 100644 --- a/git.c +++ b/git.c @@ -557,6 +557,7 @@ static struct cmd_struct commands[] = { { "commit-graph", cmd_commit_graph, RUN_SETUP }, { "commit-tree", cmd_commit_tree, RUN_SETUP }, { "config", cmd_config, RUN_SETUP_GENTLY | DELAY_PAGER_CONFIG }, + { "config-batch", cmd_config_batch, RUN_SETUP_GENTLY }, { "count-objects", cmd_count_objects, RUN_SETUP }, { "credential", cmd_credential, RUN_SETUP_GENTLY | NO_PARSEOPT }, { "credential-cache", cmd_credential_cache }, diff --git a/meson.build b/meson.build index dd52efd1c87574..040bc32c2dc3eb 100644 --- a/meson.build +++ b/meson.build @@ -582,6 +582,7 @@ builtin_sources = [ 'builtin/commit-tree.c', 'builtin/commit.c', 'builtin/config.c', + 'builtin/config-batch.c', 'builtin/count-objects.c', 'builtin/credential-cache--daemon.c', 'builtin/credential-cache.c', diff --git a/t/meson.build b/t/meson.build index 459c52a48972e4..0e9f1826f8b948 100644 --- a/t/meson.build +++ b/t/meson.build @@ -186,6 +186,7 @@ integration_tests = [ 't1309-early-config.sh', 't1310-config-default.sh', 't1311-config-optional.sh', + 't1312-config-batch.sh', 't1350-config-hooks-path.sh', 't1400-update-ref.sh', 't1401-symbolic-ref.sh', diff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh new file mode 100755 index 00000000000000..f59ba4a0f3f1dc --- /dev/null +++ b/t/t1312-config-batch.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +test_description='Test git config-batch' + +. ./test-lib.sh + +test_expect_success 'help text' ' + test_must_fail git config-batch -h >out && + grep usage out +' + +test_done From 051063754afa1592f6d01eb25a50ae380a958fd4 Mon Sep 17 00:00:00 2001 From: Derrick Stolee Date: Sat, 17 Jan 2026 13:16:55 -0500 Subject: [PATCH 2/4] config-batch: create parse loop and unknown command As we build new features in the config-batch command, we define the plaintext protocol with line-by-line output and responses. To think to the future, we make sure that the protocol has a clear way to respond to an unknown command or an unknown version of that command. Signed-off-by: Derrick Stolee --- Documentation/git-config-batch.adoc | 23 ++++- builtin/config-batch.c | 130 +++++++++++++++++++++++++++- t/t1312-config-batch.sh | 19 +++- 3 files changed, 167 insertions(+), 5 deletions(-) diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc index dfa0bd83e25f61..9ca04b0c1eafd2 100644 --- a/Documentation/git-config-batch.adoc +++ b/Documentation/git-config-batch.adoc @@ -13,7 +13,28 @@ SYNOPSIS DESCRIPTION ----------- -TODO +Tools frequently need to change their behavior based on values stored in +Git's configuration files. These files may have complicated conditions +for including extra files, so it is difficult to produce an independent +parser. To avoid executing multiple processes to discover or modify +multiple configuration values, the `git config-batch` command allows a +single process to handle multiple requests using a machine-parseable +interface across `stdin` and `stdout`. + +PROTOCOL +-------- +By default, the protocol uses line feeds (`LF`) to signal the end of a +command over `stdin` or a response over `stdout`. + +The protocol will be extended in the future, and consumers should be +resilient to older Git versions not understanding the latest command +set. Thus, if the Git version includes the `git config-batch` builtin +but doesn't understand an input command, it will return a single line +response: + +``` +unknown_command LF +``` SEE ALSO -------- diff --git a/builtin/config-batch.c b/builtin/config-batch.c index ea4f408ecb45d6..f86085bb5b54db 100644 --- a/builtin/config-batch.c +++ b/builtin/config-batch.c @@ -3,17 +3,141 @@ #include "config.h" #include "environment.h" #include "parse-options.h" +#include "strbuf.h" +#include "string-list.h" static const char *const builtin_config_batch_usage[] = { N_("git config-batch "), NULL }; +#define UNKNOWN_COMMAND "unknown_command" + +static int emit_response(const char *response, ...) +{ + va_list params; + const char *token; + + printf("%s", response); + + va_start(params, response); + while ((token = va_arg(params, const char *))) + printf(" %s", token); + va_end(params); + + printf("\n"); + fflush(stdout); + return 0; +} + +/** + * A function pointer type for defining a command. The function is + * responsible for handling different versions of the command name. + * + * These functions should only return a negative value if they result + * in such a catastrophic failure that the process should end. + * + * Return 0 on success. + */ +typedef int (*command_fn)(struct repository *repo, + int argc, const char **argv); + +static int unknown_command(struct repository *repo UNUSED, + int argc UNUSED, const char **argv UNUSED) +{ + return emit_response(UNKNOWN_COMMAND, NULL); +} + +struct command { + const char *name; + command_fn fn; + int version; +}; + +static struct command commands[] = { + /* unknown_command must be last. */ + { + .name = "", + .fn = unknown_command, + }, +}; + +#define COMMAND_COUNT ((size_t)(sizeof(commands) / sizeof(*commands))) + +/** + * Process a single line from stdin and process the command. + * + * Returns 0 on successful processing of command, including the + * unknown_command output. + * + * Returns 1 on natural exit due to exist signal of empty line. + * + * Returns negative value on other catastrophic error. + */ +static int process_command(struct repository *repo) +{ + static struct strbuf line = STRBUF_INIT; + struct string_list tokens = STRING_LIST_INIT_NODUP; + const char *command; + int version; + int argc = 0; + const char **argv = NULL; + int res = 0; + + strbuf_getline(&line, stdin); + + if (!line.len) + return 1; + + string_list_split_in_place(&tokens, line.buf, " ", -1); + + if (tokens.nr < 2) { + res = error(_("expected at least 2 tokens, got %"PRIuMAX), tokens.nr); + goto cleanup; + } + + command = tokens.items[0].string; + + if (!git_parse_int(tokens.items[1].string, &version)) { + res = error(_("unable to parse '%s' to integer"), + tokens.items[1].string); + goto cleanup; + } + + argc = tokens.nr - 2; + CALLOC_ARRAY(argv, argc + 1); + + for (size_t i = 2; i < tokens.nr; i++) + argv[i - 2] = tokens.items[i].string; + + for (size_t i = 0; i < COMMAND_COUNT; i++) { + /* + * Run the ith command if we have hit the unknown + * command or if the name and version match. + */ + if (!commands[i].name[0] || + (!strcmp(command, commands[i].name) && + commands[i].version == version)) { + res = commands[i].fn(repo, argc, argv); + goto cleanup; + } + } + + BUG(_("scanned to end of command list, including 'unknown_command'")); + +cleanup: + free(argv); + strbuf_reset(&line); + string_list_clear(&tokens, 0); + return res; +} + int cmd_config_batch(int argc, const char **argv, const char *prefix, struct repository *repo) { + int res = 0; struct option options[] = { OPT_END(), }; @@ -26,5 +150,9 @@ int cmd_config_batch(int argc, repo_config(repo, git_default_config, NULL); - return 0; + while (!(res = process_command(repo))); + + if (res == 1) + return 0; + die(_("an unrecoverable error occurred during command execution")); } diff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh index f59ba4a0f3f1dc..f60ef35e38d7c1 100755 --- a/t/t1312-config-batch.sh +++ b/t/t1312-config-batch.sh @@ -4,9 +4,22 @@ test_description='Test git config-batch' . ./test-lib.sh -test_expect_success 'help text' ' - test_must_fail git config-batch -h >out && - grep usage out +test_expect_success 'no commands' ' + echo | git config-batch >out && + test_must_be_empty out +' + +test_expect_success 'unknown_command' ' + echo unknown_command >expect && + echo "bogus 1 line of tokens" >in && + git config-batch >out in && + test_must_fail git config-batch 2>err Date: Sat, 17 Jan 2026 14:27:46 -0500 Subject: [PATCH 3/4] config-batch: implement get v1 The 'get' command for the 'git config-batch' builtin is the first command and is currently at version 1. It returns at most one value, the same as 'git config --get ' with optional value-based filtering. The documentation and tests detail the specifics of how to format requests of this format and how to parse the results. Future versions could consider multi-valued responses or regex-based key matching. For the sake of incremental exploration of the potential in the 'git config-batch' command, this is the only implementation being presented in the first patch series. Future extensions could include a '-z' parameter that uses NUL bytes in the command and output format to allow for spaces or newlines in the input or newlines in the output. Signed-off-by: Derrick Stolee --- Documentation/git-config-batch.adoc | 51 +++++++- builtin/config-batch.c | 190 ++++++++++++++++++++++++++++ config.h | 3 + t/t1312-config-batch.sh | 91 +++++++++++++ 4 files changed, 333 insertions(+), 2 deletions(-) diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc index 9ca04b0c1eafd2..1cbe2f304474c1 100644 --- a/Documentation/git-config-batch.adoc +++ b/Documentation/git-config-batch.adoc @@ -32,9 +32,56 @@ set. Thus, if the Git version includes the `git config-batch` builtin but doesn't understand an input command, it will return a single line response: -``` +------------ unknown_command LF -``` +------------ + +These are the commands that are currently understood: + +`get` version 1:: + The `get` command searches the config key-value pairs within a + given `` for values that match the fixed `` and + filters the resulting value based on an optional ``. + This can either be a regex or a fixed value. The command format + is one of the following formats: ++ +------------ +get 1 +get 1 regex +get 1 fixed-value +------------ ++ +The `` value can be one of `inherited`, `system`, `global`, +`local`, `worktree`, `submodule`, or `command`. If `inherited`, then all +config key-value pairs will be considered regardless of scope. Otherwise, +only the given scope will be considered. ++ +If no optional arguments are given, then the value will not be filtered +by any pattern matching. If `regex` is specified, then `` +is interpreted as a regular expression for matching against stored +values, similar to specifying a value to `get config --get `. +If `fixed-value` is specified, then `` is checked for an exact +match against the key-value pairs, simmilar to `git config --get +--fixed-value `. ++ +At mmost one key-value pair is returned, that being the last key-value +pair in the standard config order by scope and sequence within each scope. ++ +If a key-value pair is found, then the following output is given: ++ +------------ +get found +------------ ++ +If no matching key-value pair is found, then the following output is +given: ++ +------------ +get missing [|] +------------ ++ +where `` or `` is only supplied if provided in +the command. SEE ALSO -------- diff --git a/builtin/config-batch.c b/builtin/config-batch.c index f86085bb5b54db..9b36a0f7f36e3c 100644 --- a/builtin/config-batch.c +++ b/builtin/config-batch.c @@ -12,6 +12,8 @@ static const char *const builtin_config_batch_usage[] = { }; #define UNKNOWN_COMMAND "unknown_command" +#define GET_COMMAND "get" +#define COMMAND_PARSE_ERROR "command_parse_error" static int emit_response(const char *response, ...) { @@ -30,6 +32,11 @@ static int emit_response(const char *response, ...) return 0; } +static int command_parse_error(const char *command) +{ + return emit_response(COMMAND_PARSE_ERROR, command, NULL); +} + /** * A function pointer type for defining a command. The function is * responsible for handling different versions of the command name. @@ -48,6 +55,184 @@ static int unknown_command(struct repository *repo UNUSED, return emit_response(UNKNOWN_COMMAND, NULL); } +enum value_match_mode { + MATCH_ALL, + MATCH_EXACT, + MATCH_REGEX, +}; + +struct get_command_1_data { + /* parameters */ + const char *key; + enum config_scope scope; + enum value_match_mode mode; + const char *value; + regex_t *value_pattern; + + /* data along the way, for single values. */ + char *found; + enum config_scope found_scope; +}; + +static int get_command_1_cb(const char *key, const char *value, + const struct config_context *context, + void *data) +{ + struct get_command_1_data *d = data; + + if (strcasecmp(key, d->key)) + return 0; + + if (d->scope != CONFIG_SCOPE_UNKNOWN && + d->scope != context->kvi->scope) + return 0; + + switch (d->mode) { + case MATCH_EXACT: + if (strcasecmp(value, d->value)) + return 0; + break; + + case MATCH_REGEX: + if (regexec(d->value_pattern, value, 0, NULL, 0)) + return 0; + break; + + default: + break; + } + + free(d->found); + d->found = xstrdup(value); + d->found_scope = context->kvi->scope; + return 0; +} + +static const char *scope_str(enum config_scope scope) +{ + switch (scope) { + case CONFIG_SCOPE_UNKNOWN: + return "unknown"; + + case CONFIG_SCOPE_SYSTEM: + return "system"; + + case CONFIG_SCOPE_GLOBAL: + return "global"; + + case CONFIG_SCOPE_LOCAL: + return "local"; + + case CONFIG_SCOPE_WORKTREE: + return "worktree"; + + case CONFIG_SCOPE_SUBMODULE: + return "submodule"; + + case CONFIG_SCOPE_COMMAND: + return "command"; + + default: + BUG("invalid config scope"); + } +} + +static int parse_scope(const char *str, enum config_scope *scope) +{ + if (!strcmp(str, "inherited")) { + *scope = CONFIG_SCOPE_UNKNOWN; + return 0; + } + + for (enum config_scope s = 0; s < CONFIG_SCOPE__NR; s++) { + if (!strcmp(str, scope_str(s))) { + *scope = s; + return 0; + } + } + + return -1; +} + +/** + * 'get' command, version 1. + * + * Positional arguments should be of the form: + * + * [0] scope ("system", "global", "local", "worktree", "command", "submodule", or "inherited") + * [1] config key + * [2*] multi-mode ("all", "regex", "fixed-value") + * [3*] value regex OR value string + * + * [N*] indicates optional parameters that are not needed. + */ +static int get_command_1(struct repository *repo, int argc, const char **argv) +{ + struct get_command_1_data data = { + .found = NULL, + .mode = MATCH_ALL, + }; + int res = 0; + + if (argc < 2 || argc >= 5) + goto parse_error; + + if (parse_scope(argv[0], &data.scope)) + goto parse_error; + + data.key = argv[1]; + + if (argc >= 3) { + if (!strcmp(argv[2], "regex")) + data.mode = MATCH_REGEX; + else if (!strcmp(argv[2], "fixed-value")) + data.mode = MATCH_EXACT; + else + goto parse_error; + } + + if (data.mode == MATCH_REGEX) { + if (argc < 4) + goto parse_error; + + data.value = argv[3]; + + CALLOC_ARRAY(data.value_pattern, 1); + if (regcomp(data.value_pattern, argv[3], REG_EXTENDED)) { + FREE_AND_NULL(data.value_pattern); + goto parse_error; + } + } else if (data.mode == MATCH_EXACT) { + if (argc < 4) + goto parse_error; + + data.value = argv[3]; + } + + repo_config(repo, get_command_1_cb, &data); + + if (data.found) + res = emit_response(GET_COMMAND, "found", data.key, + scope_str(data.found_scope), data.found, + NULL); + else + res = emit_response(GET_COMMAND, "missing", data.key, data.value, NULL); + + goto cleanup; + + +parse_error: + res = command_parse_error(GET_COMMAND); + +cleanup: + if (data.value_pattern) { + regfree(data.value_pattern); + free(data.value_pattern); + } + free(data.found); + return res; +} + struct command { const char *name; command_fn fn; @@ -55,6 +240,11 @@ struct command { }; static struct command commands[] = { + { + .name = GET_COMMAND, + .fn = get_command_1, + .version = 1, + }, /* unknown_command must be last. */ { .name = "", diff --git a/config.h b/config.h index ba426a960af9f4..966a228f0e1a39 100644 --- a/config.h +++ b/config.h @@ -44,6 +44,9 @@ enum config_scope { CONFIG_SCOPE_WORKTREE, CONFIG_SCOPE_COMMAND, CONFIG_SCOPE_SUBMODULE, + + /* Must be last */ + CONFIG_SCOPE__NR }; const char *config_scope_name(enum config_scope scope); diff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh index f60ef35e38d7c1..53104cbd34b299 100755 --- a/t/t1312-config-batch.sh +++ b/t/t1312-config-batch.sh @@ -22,4 +22,95 @@ test_expect_success 'failed to parse version' ' test_grep BAD_VERSION err ' +test_expect_success 'get inherited config' ' + test_when_finished git config --unset test.key && + + git config test.key "test value with spaces" && + + echo "get 1 inherited test.key" >in && + echo "get found test.key local test value with spaces" >expect && + git config-batch >out in && + echo "get missing test.key" >expect && + git config-batch >out in <<-\EOF && + get 1 inherited test.key regex .*1.* + get 1 inherited test.key regex [a-z]2.* + get 1 inherited test.key regex .*3e + get 1 inherited test.key regex 4.* + get 1 inherited test.key regex .*5.* + get 1 inherited test.key regex .*6.* + EOF + + cat >expect <<-\EOF && + get found test.key system on1e + get found test.key global t2wo + get found test.key local thre3e + get found test.key worktree 4four + get found test.key command five5 + get missing test.key .*6.* + EOF + + git -c test.key=five5 config-batch >out in <<-\EOF && + get 1 inherited test.key fixed-value one + get 1 inherited test.key fixed-value two + get 1 inherited test.key fixed-value three + get 1 inherited test.key fixed-value four + get 1 inherited test.key fixed-value five + get 1 inherited test.key fixed-value six + EOF + + cat >expect <<-\EOF && + get found test.key system one + get found test.key global two + get found test.key local three + get found test.key worktree four + get found test.key command five + get missing test.key six + EOF + + git -c test.key=five config-batch >out Date: Sun, 18 Jan 2026 13:49:35 -0500 Subject: [PATCH 4/4] config-batch: create 'help' command Tools that use the 'git config-batch' tool will want to know which commands are available in the current Git version. Having a 'help' command assists greatly to give a clear set of available commands and their versions. Signed-off-by: Derrick Stolee --- Documentation/git-config-batch.adoc | 17 +++++++++++++++ builtin/config-batch.c | 33 +++++++++++++++++++++++++++++ t/t1312-config-batch.sh | 13 ++++++++++++ 3 files changed, 63 insertions(+) diff --git a/Documentation/git-config-batch.adoc b/Documentation/git-config-batch.adoc index 1cbe2f304474c1..3e2165824fc5d9 100644 --- a/Documentation/git-config-batch.adoc +++ b/Documentation/git-config-batch.adoc @@ -38,6 +38,23 @@ unknown_command LF These are the commands that are currently understood: +`help` version 1:: + The `help` command lists the currently-available commands in + this version of Git. The output is multi-line, but the first + line provides the count of possible commands via `help count `. + The next `` lines are of the form `help ` + to state that this Git version supports that `` at + version ``. Note that the same command may have multiple + available versions. ++ +Here is the currentl output of the help text at the latest version: ++ +------------ +help count 2 +help help 1 +help get 1 +------------ + `get` version 1:: The `get` command searches the config key-value pairs within a given `` for values that match the fixed `` and diff --git a/builtin/config-batch.c b/builtin/config-batch.c index 9b36a0f7f36e3c..3f85a4ca8b4420 100644 --- a/builtin/config-batch.c +++ b/builtin/config-batch.c @@ -12,6 +12,7 @@ static const char *const builtin_config_batch_usage[] = { }; #define UNKNOWN_COMMAND "unknown_command" +#define HELP_COMMAND "help" #define GET_COMMAND "get" #define COMMAND_PARSE_ERROR "command_parse_error" @@ -55,6 +56,9 @@ static int unknown_command(struct repository *repo UNUSED, return emit_response(UNKNOWN_COMMAND, NULL); } +static int help_command_1(struct repository *repo, + int argc, const char **argv); + enum value_match_mode { MATCH_ALL, MATCH_EXACT, @@ -240,6 +244,11 @@ struct command { }; static struct command commands[] = { + { + .name = HELP_COMMAND, + .fn = help_command_1, + .version = 1, + }, { .name = GET_COMMAND, .fn = get_command_1, @@ -254,6 +263,30 @@ static struct command commands[] = { #define COMMAND_COUNT ((size_t)(sizeof(commands) / sizeof(*commands))) + +static int help_command_1(struct repository *repo UNUSED, + int argc UNUSED, const char **argv UNUSED) +{ + struct strbuf fmt_str = STRBUF_INIT; + + strbuf_addf(&fmt_str, "%"PRIuMAX, COMMAND_COUNT - 1); + emit_response(HELP_COMMAND, "count", fmt_str.buf, NULL); + strbuf_reset(&fmt_str); + + for (size_t i = 0; i < COMMAND_COUNT; i++) { + /* Halt at unknown command. */ + if (!commands[i].name[0]) + break; + + strbuf_addf(&fmt_str, "%d", commands[i].version); + emit_response(HELP_COMMAND, commands[i].name, fmt_str.buf, NULL); + strbuf_reset(&fmt_str); + } + + strbuf_release(&fmt_str); + return 0; +} + /** * Process a single line from stdin and process the command. * diff --git a/t/t1312-config-batch.sh b/t/t1312-config-batch.sh index 53104cbd34b299..ce81494ee40eb7 100755 --- a/t/t1312-config-batch.sh +++ b/t/t1312-config-batch.sh @@ -16,6 +16,19 @@ test_expect_success 'unknown_command' ' test_cmp expect out ' +test_expect_success 'help command' ' + echo "help 1" >in && + + cat >expect <<-\EOF && + help count 2 + help help 1 + help get 1 + EOF + + git config-batch >out in && test_must_fail git config-batch 2>err