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..bdfd872d65d02f --- /dev/null +++ b/Documentation/git-config-batch.adoc @@ -0,0 +1,214 @@ +git-config-batch(1) +=================== + +NAME +---- +git-config-batch - Get and set options using machine-parseable interface + + +SYNOPSIS +-------- +[verse] +'git config-batch' + +DESCRIPTION +----------- +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`. + +OPTIONS +------- + +`-z`:: + If specified, then use the NUL-terminated input and output + format instead of the space and newline format. This format is + useful when the strings involved may include spaces or newlines. + See PROTOCOL for more details. + +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 +------------ + +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 1 count `. + The next `` lines are of the form `help 1 ` + to state that this Git version supports that `` at + version ``. Note that the same command may have multiple + available versions. ++ +Here is the current output of the help text at the latest version: ++ +------------ +help 1 count 2 +help 1 help 1 +help 1 get 1 +------------ + +`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 arg:regex +get 1 arg: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 `arg:regex` is specified, then the rest of +the line is considered a single string, ``, and is +interpreted as a regular expression for matching against stored values, +similar to specifying a value to `get config --get ""`. +If `arg:fixed-value` is specified, then the rest of the line is +considered a single string, ``, and 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 1 found +------------ ++ +If no matching key-value pair is found, then the following output is +given: ++ +------------ +get 1 missing [|] +------------ ++ +where `` or `` is only supplied if provided in +the command. + +`set` version 1:: + The `set` command writes a single key-value pair to a config + file. It specifies which file by a `` parameter from + among `system`, `global`, `local`, and `worktree`. The `` + is the next positional argument. The remaining data in the line + is provided as the `` to assign the config. ++ +------------ +set 1 +------------ ++ +These uses will match the behavior of `git config --set -- +`. Note that replacing all values with the `--all` option or +matching specific value patterns are not supported by this command. ++ +The response of these commands will include a `success` message if the +value is written as expected or `failed` if an unexpected failure +occurs: ++ +------------ +set 1 success +set 1 failed +------------ + +`unset` version 1:: + The `unset` command removes a single value from a config file. + It specifies which file by a `` parameter from among + `system`, `global`, `local`, and `worktree`. The `` is the + next positional argument. There could be two additional + arguments used to match specific config values, where the first + is either `arg:regex` or `arg:fixed-value` to specify the type + of match. ++ +------------ +unset 1 +unset 1 arg:regex +unset 1 arg:fixed-value +------------ ++ +These uses will match the behavior of `git config --unset -- ` +with the additional arguments of `` if `arg:regex` is +given or `--fixed-value ` if `arg:fixed-value` is given. ++ +The response of these commands will include a `success` message +if matched values are found and removed as expected or `failed` if an +unexpected failure occurs: ++ +------------ +unset 1 success +unset 1 failed +------------ + +NUL-Terminated Format +~~~~~~~~~~~~~~~~~~~~~ + +When `-z` is given, the protocol changes in some structural ways. + +First, each command is terminated with two NUL bytes, providing a clear +boundary between commands regardless of future possibilities of new +command formats. + +Second, any time that a space _would_ be used to partition tokens in a +command, a NUL byte is used instead. Further, each token is prefixed +with `:` where `` is a decimal representation of the length of +the string between the `:` and the next NUL byte. Any disagreement in +these lengths is treated as a parsing error. This use of a length does +imply that "`0:`" is the representation of an empty string, if relevant. + +The decimal representation must have at most five numerals, thus the +maximum length of a string token can have 99999 characters. + +For example, the `get` command, version 1, could have any of the +following forms: + +------------ +3:get NUL 1:1 NUL 5:local NUL 14:key.with space NUL NUL +3:get NUL 1:1 NUL 9:inherit NUL 8:test.key NUL 9:arg:regex NUL 6:.*\ .* NUL NUL +3:get NUL 1:1 NUL 6:global NUL 8:test.key NUL 15:arg:fixed-value NUL 3:a b NUL NUL +------------ + +The output is modified similarly, such as the following output examples, +as if the input has a parse error, a valid `help` command, a `get` +command that had a match, and a `get` command that did not match. + +------------ +15:unknown_command NUL NUL +4:help NUL 1:1 NUL 5:count NUL 1:2 NUL NUL +4:help NUL 1:1 NUL 4:help NUL 1:1 NUL NUL +4:help NUL 1:1 NUL 3:get NUL 1:1 NUL NUL +3:get NUL 1:1 NUL 5:found NUL 8:test.key NUL 5:value NUL NUL +3:get NUL 1:1 NUL 7:missing NUL 8:test.key NUL NUL +------------ + + +SEE ALSO +-------- +linkgit:git-config[1] + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/meson.build b/Documentation/meson.build index f02dbc20cbcb86..f5ad1179213682 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -29,6 +29,7 @@ manpages = { 'git-commit-tree.adoc' : 1, 'git-commit.adoc' : 1, 'git-config.adoc' : 1, + 'git-config-batch.adoc' : 1, 'git-count-objects.adoc' : 1, 'git-credential-cache--daemon.adoc' : 1, 'git-credential-cache.adoc' : 1, diff --git a/Documentation/technical/config-batch.adoc b/Documentation/technical/config-batch.adoc new file mode 100644 index 00000000000000..dbd614ad4f7dd2 --- /dev/null +++ b/Documentation/technical/config-batch.adoc @@ -0,0 +1,70 @@ +Git Config-Batch Design Notes +============================= + +The `git config-batch` builtin has a robust protocol for parsing multiple +commands over `stdin` and providing structured output over `stdout`. The +intended use is for scripts or third-party software to interact with the +config settings of a repository multiple times within the same Git process. +The protocol is built with versioning that allows the consumer to know when +a certain command is available and to fall back to single-use `git config` +processes if the installed Git version does not have the latest commands +at the required versions. + +Recommended interaction pattern +------------------------------- + +This section provides a guide for ideal interaction with the `git +config-batch` command and its protocol. + +For maximum compatibility, do not attempt parsing the output of `git +version` to determine which commands are available. Instead, first check +if the `git config-batch` command succeeds and does not die immediately +due to the builtin being unavailable. Then, use the v1 of the `help` +command to get a list of available commands and versions. Use this list to +determine if your capabilities are available or should be replaced with an +appropriate `git config` single-use process. + +Further, all automated tooling would be better off using the +NUL-terminated format instead of the whitespace-delimited format, in case +config keys contain spaces or config values contain newlines. The +whitespace-delimited version is available for simpler integration and +human inspection. + +Current commands +---------------- + +See the documentation in linkgit::config-batch[1] for the latest set of +available commands and their protocols. + +Future commands +--------------- + +The following modes of `git config` are not currently available as commands +in `git config-batch`, but are planned for future integration: + +`git config list [--]`:: + Getting all values, regardless of config key, would require a + multi-valued output similar to the `help` command. This tool will + likely assume advanced options such as `--show-origin`. + +`git config set [--] `:: + It will be desirable to set a config key at a given scope as a + single value, replacing the current value at that scope, if it + exists and is a single value. A `set` command could satisfy this + purpose. + +`git config set --all [|--fixed-value=] `:: + When replacing multiple values, it may be necessary to have a different + output describing the places those values were set, so it may need to + be implemented via a `set-all` command to differentiate from a `set` + command. + +`git config unset `:: + +`git config unset --all [|--fixed-value=] `:: + +`git config get --all --rexexp []`:: + +`--replace-all` option:: + +`--type=` option:: diff --git a/Makefile b/Makefile index 8aa489f3b6812f..aa3868e5134119 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..5f5a19635ee57c 100644 --- a/builtin.h +++ b/builtin.h @@ -68,12 +68,18 @@ * * . 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 'git-foo.adoc' to the manpages list in 'Documentation/meson.build'. + * * . Add an entry for `git-foo` to `command-list.txt`. * * . Add an entry for `/git-foo` to `.gitignore`. @@ -167,6 +173,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..25a942ba613590 --- /dev/null +++ b/builtin/config-batch.c @@ -0,0 +1,772 @@ +#define USE_THE_REPOSITORY_VARIABLE +#include "builtin.h" +#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 +}; + +static int zformat = 0; + +#define UNKNOWN_COMMAND "unknown_command" +#define HELP_COMMAND "help" +#define GET_COMMAND "get" +#define SET_COMMAND "set" +#define UNSET_COMMAND "unset" +#define COMMAND_PARSE_ERROR "command_parse_error" + +static void print_word(const char *word, int start) +{ + if (zformat) { + printf("%"PRIu32":%s", (uint32_t)strlen(word), word); + fputc(0, stdout); + } else if (start) + printf("%s", word); + else + printf(" %s", word); +} + +static int emit_response(const char *response, ...) +{ + va_list params; + const char *token; + + print_word(response, 1); + + va_start(params, response); + while ((token = va_arg(params, const char *))) + print_word(token, 0); + va_end(params); + + if (zformat) + fputc(0, stdout); + else + printf("\n"); + fflush(stdout); + 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. + * + * Provides the remaining 'data' for the command, to be parsed by + * the function as needed according to its parsing rules. + * + * These functions should only return a negative value if they result + * in such a catastrophic failure that the process should end. + * + * Return 0 on success. + */ +typedef int (*command_fn)(struct repository *repo, + const char *prefix, + char *data, size_t data_len); + +static int unknown_command(struct repository *repo UNUSED, + const char *prefix UNUSED, + char *data UNUSED, size_t data_len UNUSED) +{ + return emit_response(UNKNOWN_COMMAND, NULL); +} + +/* + * Parse the next token using the NUL-byte format. + */ +static size_t parse_ztoken(char **data, size_t *data_len, + char **token, int *err) +{ + size_t i = 0, token_len; + + while (i < *data_len && (*data)[i] != ':') { + if ((*data)[i] < '0' || (*data)[i] > '9') { + goto parse_error; + } + i++; + } + + if (i >= *data_len || (*data)[i] != ':' || i > 5) + goto parse_error; + + (*data)[i] = 0; + token_len = atoi(*data); + + if (token_len + i + 1 >= *data_len) + goto parse_error; + + *token = *data + i + 1; + *data_len = *data_len - (i + 1); + + /* check for early NULs. */ + for (i = 0; i < token_len; i++) { + if (!(*token)[i]) + goto parse_error; + } + /* check for matching NUL. */ + if ((*token)[token_len]) + goto parse_error; + + *data = *token + token_len + 1; + *data_len = *data_len - (token_len + 1); + return token_len; + +parse_error: + *err = 1; + *token = NULL; + return 0; +} + +static size_t parse_whitespace_token(char **data, size_t *data_len, + char **token, int *err UNUSED) +{ + size_t i = 0; + + *token = *data; + + while (i < *data_len && (*data)[i] && (*data)[i] != ' ') + i++; + + if (i >= *data_len) { + *data_len = 0; + *data = NULL; + return i; + } + + (*data)[i] = 0; + *data_len = (*data_len) - (i + 1); + *data = *data + (i + 1); + return i; +} + +/** + * Given the remaining data line and its size, attempt to extract + * a token. When the token delimiter is determined, the data + * string is mutated to insert a NUL byte at the end of the token. + * The data pointer is mutated to point at the next character (or + * set to NULL if that exceeds the string length). The data_len + * value is mutated to subtract the length of the discovered + * token. + * + * The returned value is the length of the token that was + * discovered. + * + * The 'token' pointer is used to set the start of the token. + * In the whitespace format, this is always the input value of + * 'data' but in the NUL-terminated format this follows an ":" + * prefix. + * + * In the case of the NUL-terminated format, a bad parse of the + * decimal length or a mismatch of the decimal length and the + * length of the following NUL-terminated string will result in + * the value pointed at by 'err' to be set to 1. + */ +static size_t parse_token(char **data, size_t *data_len, + char **token, int *err) +{ + if (!*data_len) + return 0; + if (zformat) + return parse_ztoken(data, data_len, token, err); + return parse_whitespace_token(data, data_len, token, err); +} + +static int help_command_1(struct repository *repo, + const char *prefix UNUSED, + char *data, size_t data_len); + +enum value_match_mode { + MATCH_ALL, + MATCH_EXACT, + MATCH_REGEX, +}; + +struct get_command_1_data { + /* parameters */ + char *key; + enum config_scope scope; + enum value_match_mode mode; + + /* optional parameters */ + 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 ("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, + const char *prefix UNUSED, + char *data, + size_t data_len) +{ + struct get_command_1_data gc_data = { + .found = NULL, + .mode = MATCH_ALL, + }; + int res = 0, err = 0; + char *token; + size_t token_len; + + if (!parse_token(&data, &data_len, &token, &err) || err) + goto parse_error; + + if (parse_scope(token, &gc_data.scope)) + goto parse_error; + + if (!parse_token(&data, &data_len, &gc_data.key, &err) || err) + goto parse_error; + + token_len = parse_token(&data, &data_len, &token, &err); + if (err) + goto parse_error; + + if (token_len && !strncmp(token, "arg:", 4)) { + if (!strcmp(token + 4, "regex")) + gc_data.mode = MATCH_REGEX; + else if (!strcmp(token + 4, "fixed-value")) + gc_data.mode = MATCH_EXACT; + else + goto parse_error; /* unknown arg. */ + + /* Use the remaining data as the value string. */ + if (!zformat) + gc_data.value = data; + else { + parse_token(&data, &data_len, &gc_data.value, &err); + if (err) + goto parse_error; + } + + if (gc_data.mode == MATCH_REGEX) { + CALLOC_ARRAY(gc_data.value_pattern, 1); + if (regcomp(gc_data.value_pattern, gc_data.value, + REG_EXTENDED)) { + FREE_AND_NULL(gc_data.value_pattern); + goto parse_error; + } + } + } else if (token_len) { + /* + * If we have remaining tokens not starting in "arg:", + * then we don't understand them. + */ + goto parse_error; + } + + repo_config(repo, get_command_1_cb, &gc_data); + + if (gc_data.found) + res = emit_response(GET_COMMAND, "1", "found", gc_data.key, + scope_str(gc_data.found_scope), + gc_data.found, + NULL); + else + res = emit_response(GET_COMMAND, "1", "missing", gc_data.key, + gc_data.value, NULL); + + goto cleanup; + + +parse_error: + res = command_parse_error(GET_COMMAND); + +cleanup: + if (gc_data.value_pattern) { + regfree(gc_data.value_pattern); + free(gc_data.value_pattern); + } + free(gc_data.found); + return res; +} + + +/** + * 'set' command, version 1. + * + * Positional arguments should be of the form: + * + * [0] scope ("system", "global", "local", or "worktree") + * [1] config key + * [2] config value + */ +static int set_command_1(struct repository *repo, + const char *prefix, + char *data, + size_t data_len) +{ + int res = 0, err = 0; + enum config_scope scope = CONFIG_SCOPE_UNKNOWN; + char *token = NULL, *key = NULL, *value = NULL; + struct config_location_options locopts = CONFIG_LOCATION_OPTIONS_INIT; + + if (!parse_token(&data, &data_len, &token, &err) || err) + goto parse_error; + + if (parse_scope(token, &scope) || + scope == CONFIG_SCOPE_UNKNOWN || + scope == CONFIG_SCOPE_SUBMODULE || + scope == CONFIG_SCOPE_COMMAND) + goto parse_error; + + if (!parse_token(&data, &data_len, &key, &err) || err) + goto parse_error; + + /* Use the remaining data as the value string. */ + if (!zformat) + value = data; + else { + parse_token(&data, &data_len, &value, &err); + if (err) + goto parse_error; + } + + if (location_options_set_scope(&locopts, scope)) + goto parse_error; + location_options_init(repo, &locopts, prefix); + + res = repo_config_set_in_file_gently(repo, locopts.source.file, + key, NULL, value); + + if (res) + res = emit_response(SET_COMMAND, "1", "failure", + scope_str(scope), key, value, NULL); + else + res = emit_response(SET_COMMAND, "1", "success", + scope_str(scope), key, value, NULL); + + goto cleanup; + +parse_error: + res = command_parse_error(SET_COMMAND); + +cleanup: + location_options_release(&locopts); + return res; +} + +/** + * 'unset' command, version 1. + * + * Positional arguments should be of the form: + * + * [0] scope ("system", "global", "local", or "worktree") + * [1] config key + * [2] config value + * [3*] match ("regex", "fixed-value") + * [4*] value regex OR value string + * + * [N*] indicates optional parameters that are not needed. + */ +static int unset_command_1(struct repository *repo, + const char *prefix, + char *data, + size_t data_len) +{ + int res = 0, err = 0, flags = 0; + enum config_scope scope = CONFIG_SCOPE_UNKNOWN; + char *token = NULL, *key = NULL, *value_pattern = NULL; + size_t token_len; + struct config_location_options locopts = CONFIG_LOCATION_OPTIONS_INIT; + + if (!parse_token(&data, &data_len, &token, &err) || err) + goto parse_error; + + if (parse_scope(token, &scope) || + scope == CONFIG_SCOPE_UNKNOWN || + scope == CONFIG_SCOPE_SUBMODULE || + scope == CONFIG_SCOPE_COMMAND) + goto parse_error; + + if (!parse_token(&data, &data_len, &key, &err) || err) + goto parse_error; + + token_len = parse_token(&data, &data_len, &token, &err); + if (err) + goto parse_error; + + if (token_len && !strncmp(token, "arg:", 4)) { + if (!strcmp(token + 4, "fixed-value")) + flags |= CONFIG_FLAGS_FIXED_VALUE; + /* no special logic for arg:regex. */ + else if (strcmp(token + 4, "regex")) + goto parse_error; /* unknown arg. */ + + /* Use the remaining data as the value string. */ + if (!zformat) + value_pattern = data; + else { + parse_token(&data, &data_len, &value_pattern, &err); + if (err) + goto parse_error; + } + } else if (token_len) { + /* + * If we have remaining tokens not starting in "arg:", + * then we don't understand them. + */ + goto parse_error; + } + + if (location_options_set_scope(&locopts, scope)) + goto parse_error; + location_options_init(repo, &locopts, prefix); + + res = repo_config_set_multivar_in_file_gently( + repo, + locopts.source.file, + key, + /* value */ NULL, + value_pattern, + /* comment */ NULL, + flags); + + if (res) + res = emit_response(UNSET_COMMAND, "1", "failure", + scope_str(scope), key, NULL); + else + res = emit_response(UNSET_COMMAND, "1", "success", + scope_str(scope), key, NULL); + + goto cleanup; + +parse_error: + res = command_parse_error(UNSET_COMMAND); + +cleanup: + location_options_release(&locopts); + return res; +} + +struct command { + const char *name; + command_fn fn; + int version; +}; + +static struct command commands[] = { + { + .name = HELP_COMMAND, + .fn = help_command_1, + .version = 1, + }, + { + .name = GET_COMMAND, + .fn = get_command_1, + .version = 1, + }, + { + .name = SET_COMMAND, + .fn = set_command_1, + .version = 1, + }, + { + .name = UNSET_COMMAND, + .fn = unset_command_1, + .version = 1, + }, + /* unknown_command must be last. */ + { + .name = "", + .fn = unknown_command, + }, +}; + +#define COMMAND_COUNT ((size_t)(sizeof(commands) / sizeof(*commands))) + +static int help_command_1(struct repository *repo UNUSED, + const char *prefix UNUSED, + char *data UNUSED, size_t data_len UNUSED) +{ + struct strbuf fmt_str = STRBUF_INIT; + + strbuf_addf(&fmt_str, "%"PRIu32, (uint32_t)(COMMAND_COUNT - 1)); + emit_response(HELP_COMMAND, "1", "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, "1", commands[i].name, fmt_str.buf, NULL); + strbuf_reset(&fmt_str); + } + + strbuf_release(&fmt_str); + return 0; +} + +static int process_command_nul(struct repository *repo, + const char *prefix) +{ + static struct strbuf line = STRBUF_INIT; + char *data, *command, *versionstr; + size_t data_len, token_len; + int res = 0, err = 0, version = 0, getc; + char c; + + /* If we start with EOF it's not an error. */ + getc = fgetc(stdin); + if (getc == EOF) + return 1; + + do { + c = (char)getc; + strbuf_addch(&line, c); + + if (!c && line.len > 1 && !line.buf[line.len - 2]) + break; + + getc = fgetc(stdin); + + /* It's an error if we reach EOF while parsing a command. */ + if (getc == EOF) + goto parse_error; + } while (1); + + data = line.buf; + data_len = line.len - 1; + + token_len = parse_ztoken(&data, &data_len, &command, &err); + if (!token_len || err) + goto parse_error; + + token_len = parse_ztoken(&data, &data_len, &versionstr, &err); + if (!token_len || err) + goto parse_error; + + if (!git_parse_int(versionstr, &version)) { + res = error(_("unable to parse '%s' to integer"), + versionstr); + goto parse_error; + } + + 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, prefix, data, data_len); + goto cleanup; + } + } + + BUG(_("scanned to end of command list, including 'unknown_command'")); + +parse_error: + res = unknown_command(repo, prefix, NULL, 0); + +cleanup: + strbuf_release(&line); + return res; +} + +static int process_command_whitespace(struct repository *repo, + const char *prefix) +{ + static struct strbuf line = STRBUF_INIT; + struct string_list tokens = STRING_LIST_INIT_NODUP; + const char *command; + int version; + char *data = NULL; + size_t data_len = 0; + int res = 0; + + strbuf_getline(&line, stdin); + + if (!line.len) + return 1; + + /* Parse out the first two tokens, command and version. */ + string_list_split_in_place(&tokens, line.buf, " ", 2); + + if (tokens.nr < 2) { + res = error(_("expected at least 2 tokens, got %"PRIu32), + (uint32_t)tokens.nr); + goto cleanup; + } + + command = tokens.items[0].string; + + if (!git_parse_int(tokens.items[1].string, &version)) { + res = error(_("unable to parse '%s' to integer"), + tokens.items[1].string); + goto cleanup; + } + + if (tokens.nr >= 3) { + data = tokens.items[2].string; + data_len = strlen(tokens.items[2].string); + } + + for (size_t i = 0; i < COMMAND_COUNT; i++) { + /* + * Run the ith command if we have hit the unknown + * command or if the name and version match. + */ + if (!commands[i].name[0] || + (!strcmp(command, commands[i].name) && + commands[i].version == version)) { + res = commands[i].fn(repo, prefix, data, data_len); + goto cleanup; + } + } + + BUG(_("scanned to end of command list, including 'unknown_command'")); + +cleanup: + strbuf_reset(&line); + string_list_clear(&tokens, 0); + return res; +} + +/** + * 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, + const char *prefix) +{ + if (zformat) + return process_command_nul(repo, prefix); + return process_command_whitespace(repo, prefix); +} + +int cmd_config_batch(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + int res = 0; + struct option options[] = { + OPT_BOOL('z', NULL, &zformat, + N_("stdin and stdout is NUL-terminated")), + 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); + + while (!(res = process_command(repo, prefix))); + + if (res == 1) + return 0; + die(_("an unrecoverable error occurred during command execution")); +} diff --git a/builtin/config.c b/builtin/config.c index 288ebdfdaaab1c..d129b1204d0772 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -71,20 +71,6 @@ static const char *const builtin_config_edit_usage[] = { OPT_STRING('f', "file", &opts.source.file, N_("file"), N_("use given config file")), \ OPT_STRING(0, "blob", &opts.source.blob, N_("blob-id"), N_("read config from given blob object")) -struct config_location_options { - struct git_config_source source; - struct config_options options; - char *file_to_free; - int use_global_config; - int use_system_config; - int use_local_config; - int use_worktree_config; - int respect_includes_opt; -}; -#define CONFIG_LOCATION_OPTIONS_INIT { \ - .respect_includes_opt = -1, \ -} - #define CONFIG_TYPE_OPTIONS(type) \ OPT_GROUP(N_("Type")), \ OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type), \ @@ -772,93 +758,6 @@ static char *default_user_config(void) return strbuf_detach(&buf, NULL); } -static void location_options_init(struct config_location_options *opts, - const char *prefix) -{ - if (!opts->source.file) - opts->source.file = opts->file_to_free = - xstrdup_or_null(getenv(CONFIG_ENVIRONMENT)); - - if (opts->use_global_config + opts->use_system_config + - opts->use_local_config + opts->use_worktree_config + - !!opts->source.file + !!opts->source.blob > 1) { - error(_("only one config file at a time")); - exit(129); - } - - if (!startup_info->have_repository) { - if (opts->use_local_config) - die(_("--local can only be used inside a git repository")); - if (opts->source.blob) - die(_("--blob can only be used inside a git repository")); - if (opts->use_worktree_config) - die(_("--worktree can only be used inside a git repository")); - } - - if (opts->source.file && - !strcmp(opts->source.file, "-")) { - opts->source.file = NULL; - opts->source.use_stdin = 1; - opts->source.scope = CONFIG_SCOPE_COMMAND; - } - - if (opts->use_global_config) { - opts->source.file = opts->file_to_free = git_global_config(); - if (!opts->source.file) - /* - * It is unknown if HOME/.gitconfig exists, so - * we do not know if we should write to XDG - * location; error out even if XDG_CONFIG_HOME - * is set and points at a sane location. - */ - die(_("$HOME not set")); - opts->source.scope = CONFIG_SCOPE_GLOBAL; - } else if (opts->use_system_config) { - opts->source.file = opts->file_to_free = git_system_config(); - opts->source.scope = CONFIG_SCOPE_SYSTEM; - } else if (opts->use_local_config) { - opts->source.file = opts->file_to_free = repo_git_path(the_repository, "config"); - opts->source.scope = CONFIG_SCOPE_LOCAL; - } else if (opts->use_worktree_config) { - struct worktree **worktrees = get_worktrees(); - if (the_repository->repository_format_worktree_config) - opts->source.file = opts->file_to_free = - repo_git_path(the_repository, "config.worktree"); - else if (worktrees[0] && worktrees[1]) - die(_("--worktree cannot be used with multiple " - "working trees unless the config\n" - "extension worktreeConfig is enabled. " - "Please read \"CONFIGURATION FILE\"\n" - "section in \"git help worktree\" for details")); - else - opts->source.file = opts->file_to_free = - repo_git_path(the_repository, "config"); - opts->source.scope = CONFIG_SCOPE_LOCAL; - free_worktrees(worktrees); - } else if (opts->source.file) { - if (!is_absolute_path(opts->source.file) && prefix) - opts->source.file = opts->file_to_free = - prefix_filename(prefix, opts->source.file); - opts->source.scope = CONFIG_SCOPE_COMMAND; - } else if (opts->source.blob) { - opts->source.scope = CONFIG_SCOPE_COMMAND; - } - - if (opts->respect_includes_opt == -1) - opts->options.respect_includes = !opts->source.file; - else - opts->options.respect_includes = opts->respect_includes_opt; - if (startup_info->have_repository) { - opts->options.commondir = repo_get_common_dir(the_repository); - opts->options.git_dir = repo_get_git_dir(the_repository); - } -} - -static void location_options_release(struct config_location_options *opts) -{ - free(opts->file_to_free); -} - static void display_options_init(struct config_display_options *opts) { if (opts->end_nul) { @@ -885,7 +784,7 @@ static int cmd_config_list(int argc, const char **argv, const char *prefix, argc = parse_options(argc, argv, prefix, opts, builtin_config_list_usage, 0); check_argc(argc, 0, 0); - location_options_init(&location_opts, prefix); + location_options_init(the_repository, &location_opts, prefix); display_options_init(&display_opts); setup_auto_pager("config", 1); @@ -944,7 +843,7 @@ static int cmd_config_get(int argc, const char **argv, const char *prefix, value_pattern)) die(_("--url= cannot be used with --all, --regexp or --value")); - location_options_init(&location_opts, prefix); + location_options_init(the_repository, &location_opts, prefix); display_options_init(&display_opts); if (display_opts.type != TYPE_COLOR) @@ -998,7 +897,7 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix, comment = git_config_prepare_comment_string(comment_arg); - location_options_init(&location_opts, prefix); + location_options_init(the_repository, &location_opts, prefix); check_write(&location_opts.source); value = normalize_value(argv[0], argv[1], type, &default_kvi); @@ -1044,7 +943,7 @@ static int cmd_config_unset(int argc, const char **argv, const char *prefix, if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern) die(_("--fixed-value only applies with 'value-pattern'")); - location_options_init(&location_opts, prefix); + location_options_init(the_repository, &location_opts, prefix); check_write(&location_opts.source); if ((flags & CONFIG_FLAGS_MULTI_REPLACE) || value_pattern) @@ -1073,7 +972,7 @@ static int cmd_config_rename_section(int argc, const char **argv, const char *pr PARSE_OPT_STOP_AT_NON_OPTION); check_argc(argc, 2, 2); - location_options_init(&location_opts, prefix); + location_options_init(the_repository, &location_opts, prefix); check_write(&location_opts.source); ret = repo_config_rename_section_in_file(the_repository, location_opts.source.file, @@ -1103,7 +1002,7 @@ static int cmd_config_remove_section(int argc, const char **argv, const char *pr PARSE_OPT_STOP_AT_NON_OPTION); check_argc(argc, 1, 1); - location_options_init(&location_opts, prefix); + location_options_init(the_repository, &location_opts, prefix); check_write(&location_opts.source); ret = repo_config_rename_section_in_file(the_repository, location_opts.source.file, @@ -1163,7 +1062,7 @@ static int cmd_config_edit(int argc, const char **argv, const char *prefix, argc = parse_options(argc, argv, prefix, opts, builtin_config_edit_usage, 0); check_argc(argc, 0, 0); - location_options_init(&location_opts, prefix); + location_options_init(the_repository, &location_opts, prefix); check_write(&location_opts.source); ret = show_editor(&location_opts); @@ -1231,7 +1130,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix) builtin_config_usage, PARSE_OPT_STOP_AT_NON_OPTION); - location_options_init(&location_opts, prefix); + location_options_init(the_repository, &location_opts, prefix); display_options_init(&display_opts); if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && display_opts.type) { 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/config.c b/config.c index 7f6d53b4737cd8..fa72234750b187 100644 --- a/config.c +++ b/config.c @@ -35,6 +35,7 @@ #include "strvec.h" #include "trace2.h" #include "wildmatch.h" +#include "worktree.h" #include "write-or-die.h" struct config_source { @@ -3592,3 +3593,118 @@ int lookup_config(const char **mapping, int nr_mapping, const char *var) } return -1; } + +int location_options_set_scope(struct config_location_options *opts, + enum config_scope scope) +{ + switch (scope) { + case CONFIG_SCOPE_SYSTEM: + opts->use_system_config = 1; + break; + + case CONFIG_SCOPE_GLOBAL: + opts->use_global_config = 1; + break; + + case CONFIG_SCOPE_LOCAL: + opts->use_local_config = 1; + break; + + case CONFIG_SCOPE_WORKTREE: + opts->use_worktree_config = 1; + break; + + default: + return -1; + } + + return 0; +} + +void location_options_init(struct repository *repo, + struct config_location_options *opts, + const char *prefix) +{ + if (!opts->source.file) + opts->source.file = opts->file_to_free = + xstrdup_or_null(getenv(CONFIG_ENVIRONMENT)); + + if (opts->use_global_config + opts->use_system_config + + opts->use_local_config + opts->use_worktree_config + + !!opts->source.file + !!opts->source.blob > 1) { + error(_("only one config file at a time")); + exit(129); + } + + if (!startup_info->have_repository) { + if (opts->use_local_config) + die(_("--local can only be used inside a git repository")); + if (opts->source.blob) + die(_("--blob can only be used inside a git repository")); + if (opts->use_worktree_config) + die(_("--worktree can only be used inside a git repository")); + } + + if (opts->source.file && + !strcmp(opts->source.file, "-")) { + opts->source.file = NULL; + opts->source.use_stdin = 1; + opts->source.scope = CONFIG_SCOPE_COMMAND; + } + + if (opts->use_global_config) { + opts->source.file = opts->file_to_free = git_global_config(); + if (!opts->source.file) + /* + * It is unknown if HOME/.gitconfig exists, so + * we do not know if we should write to XDG + * location; error out even if XDG_CONFIG_HOME + * is set and points at a sane location. + */ + die(_("$HOME not set")); + opts->source.scope = CONFIG_SCOPE_GLOBAL; + } else if (opts->use_system_config) { + opts->source.file = opts->file_to_free = git_system_config(); + opts->source.scope = CONFIG_SCOPE_SYSTEM; + } else if (opts->use_local_config) { + opts->source.file = opts->file_to_free = repo_git_path(repo, "config"); + opts->source.scope = CONFIG_SCOPE_LOCAL; + } else if (opts->use_worktree_config) { + struct worktree **worktrees = get_worktrees(); + if (repo->repository_format_worktree_config) + opts->source.file = opts->file_to_free = + repo_git_path(repo, "config.worktree"); + else if (worktrees[0] && worktrees[1]) + die(_("--worktree cannot be used with multiple " + "working trees unless the config\n" + "extension worktreeConfig is enabled. " + "Please read \"CONFIGURATION FILE\"\n" + "section in \"git help worktree\" for details")); + else + opts->source.file = opts->file_to_free = + repo_git_path(repo, "config"); + opts->source.scope = CONFIG_SCOPE_LOCAL; + free_worktrees(worktrees); + } else if (opts->source.file) { + if (!is_absolute_path(opts->source.file) && prefix) + opts->source.file = opts->file_to_free = + prefix_filename(prefix, opts->source.file); + opts->source.scope = CONFIG_SCOPE_COMMAND; + } else if (opts->source.blob) { + opts->source.scope = CONFIG_SCOPE_COMMAND; + } + + if (opts->respect_includes_opt == -1) + opts->options.respect_includes = !opts->source.file; + else + opts->options.respect_includes = opts->respect_includes_opt; + if (startup_info->have_repository) { + opts->options.commondir = repo_get_common_dir(repo); + opts->options.git_dir = repo_get_git_dir(repo); + } +} + +void location_options_release(struct config_location_options *opts) +{ + free(opts->file_to_free); +} diff --git a/config.h b/config.h index ba426a960af9f4..f6432c1ec26e60 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); @@ -163,6 +166,29 @@ struct config_context { typedef int (*config_fn_t)(const char *, const char *, const struct config_context *, void *); +struct config_location_options { + struct git_config_source source; + struct config_options options; + char *file_to_free; + int use_global_config; + int use_system_config; + int use_local_config; + int use_worktree_config; + int respect_includes_opt; +}; +#define CONFIG_LOCATION_OPTIONS_INIT { \ + .respect_includes_opt = -1, \ +} + +int location_options_set_scope(struct config_location_options *opts, + enum config_scope scope); + +void location_options_init(struct repository *repo, + struct config_location_options *opts, + const char *prefix); + +void location_options_release(struct config_location_options *opts); + /** * Read a specific file in git-config format. * This function takes the same callback and data parameters as `repo_config`. 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..3bddbc0de3f922 --- /dev/null +++ b/t/t1312-config-batch.sh @@ -0,0 +1,372 @@ +#!/bin/sh + +test_description='Test git config-batch' + +. ./test-lib.sh + +# usage: test_zformat out +# +# Let 'in' be a z-format input but with " NUL " between tokens in +# a single command and " NUL NUL" trailing each line. +# +# The values in 'out' will be space- and newline-delimited where +# NUL-bytes would normally be output. +test_zformat () { + sed -e "s/\ NUL\ /!/g" >nullin1 && + sed -e "s/NUL//g" nullin2 && + + tr "!" "\0" nullin3 && + tr "\n" "\0" zin && + + $* zout && + + tr "\0" " " outspace && + sed "s/\ \ /\n/g" 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 in && + + cat >expect <<-\EOF && + help 1 count 4 + help 1 help 1 + help 1 get 1 + help 1 set 1 + help 1 unset 1 + EOF + + git config-batch >out in <<-\EOF && + 4:help NUL 1:1 NUL NUL + 5:bogus NUL 2:10 NUL NUL + EOF + + cat >expect <<-\EOF && + 4:help 1:1 5:count 1:4 + 4:help 1:1 4:help 1:1 + 4:help 1:1 3:get 1:1 + 4:help 1:1 3:set 1:1 + 4:help 1:1 5:unset 1:1 + 15:unknown_command + EOF + + test_zformat git config-batch -z >out in && + test_must_fail git config-batch 2>err in && + echo "get 1 found test.key local test value with spaces" >expect && + git config-batch >out in && + echo "get 1 missing test.key" >expect && + git config-batch >out in <<-\EOF && + get 1 inherited test.key arg:regex .*1.* + get 1 inherited test.key arg:regex [a-z]2.* + get 1 inherited test.key arg:regex .*3e s.* + get 1 inherited test.key arg:regex 4.* + get 1 inherited test.key arg:regex .*5.* + get 1 inherited test.key arg:regex .*6.* + EOF + + cat >expect <<-\EOF && + get 1 found test.key system on1e + get 1 found test.key global t2wo + get 1 found test.key local thre3e space + get 1 found test.key worktree 4four + get 1 found test.key command five5 + get 1 missing test.key .*6.* + EOF + + git -c test.key=five5 config-batch >out in <<-\EOF && + get 1 inherited test.key arg:fixed-value one + get 1 inherited test.key arg:fixed-value two + get 1 inherited test.key arg:fixed-value three space + get 1 inherited test.key arg:fixed-value four + get 1 inherited test.key arg:fixed-value five + get 1 inherited test.key arg:fixed-value six + EOF + + cat >expect <<-\EOF && + get 1 found test.key system one + get 1 found test.key global two + get 1 found test.key local three space + get 1 found test.key worktree four + get 1 found test.key command five + get 1 missing test.key six + EOF + + git -c test.key=five config-batch >out in <<-\EOF && + 3:get NUL 1:1 NUL 9:inherited NUL 8:test.key NUL NUL + 3:get NUL 1:1 NUL 6:global NUL 8:test.key NUL 9:arg:regex NUL 3:2.* NUL NUL + 3:get NUL 1:1 NUL 5:local NUL 8:test.key NUL 15:arg:fixed-value NUL 12:thre3e space NUL NUL + 3:get NUL 1:1 NUL 9:inherited NUL 11:key.missing NUL NUL + EOF + + cat >expect <<-\EOF && + 3:get 1:1 5:found 8:test.key 8:worktree 5:4four + 3:get 1:1 5:found 8:test.key 6:global 4:t2wo + 3:get 1:1 5:found 8:test.key 5:local 12:thre3e space + 3:get 1:1 7:missing 11:key.missing + EOF + + test_zformat git config-batch -z >out in <<-\EOF && + set 1 system test.set.system system + set 1 global test.set.global global + set 1 local test.set.local local with spaces + set 1 worktree test.set.worktree worktree + set 1 submodule test.set.submodule submodule + set 1 command test.set.command command + set 1 inherited test.set.inherited inherited + EOF + + cat >expect <<-\EOF && + set 1 success system test.set.system system + set 1 success global test.set.global global + set 1 success local test.set.local local with spaces + set 1 success worktree test.set.worktree worktree + command_parse_error set + command_parse_error set + command_parse_error set + EOF + + git config-batch out 2>err && + + test_must_be_empty err && + test_cmp expect out && + + cat >expect-values <<-EOF && + file:system-config-file system + file:global-config-file global + file:.git/config local with spaces + file:.git/config.worktree worktree + EOF + + git config get --show-origin --regexp --all test.set.* >values && + test_cmp expect-values values +' + +test_expect_success 'set config by scope with -z' ' + test_when_finished git config remove-section test.set && + GIT_CONFIG_SYSTEM=system-config-file && + GIT_CONFIG_NOSYSTEM=0 && + GIT_CONFIG_GLOBAL=global-config-file && + export GIT_CONFIG_SYSTEM && + export GIT_CONFIG_NOSYSTEM && + export GIT_CONFIG_GLOBAL && + + cat >in <<-\EOF && + 3:set NUL 1:1 NUL 6:system NUL 15:test.set.system NUL 6:system NUL NUL + 3:set NUL 1:1 NUL 6:global NUL 15:test.set.global NUL 6:global NUL NUL + 3:set NUL 1:1 NUL 5:local NUL 14:test.set.local NUL 17:local with spaces NUL NUL + 3:set NUL 1:1 NUL 8:worktree NUL 17:test.set.worktree NUL 8:worktree NUL NUL + 3:set NUL 1:1 NUL 9:submodule NUL 18:test.set.submodule NUL 9:submodule NUL NUL + 3:set NUL 1:1 NUL 7:command NUL 16:test.set.command NUL 7:command NUL NUL + 3:set NUL 1:1 NUL 9:inherited NUL 18:test.set.inherited NUL 9:inherited NUL NUL + EOF + + cat >expect <<-\EOF && + 3:set 1:1 7:success 6:system 15:test.set.system 6:system + 3:set 1:1 7:success 6:global 15:test.set.global 6:global + 3:set 1:1 7:success 5:local 14:test.set.local 17:local with spaces + 3:set 1:1 7:success 8:worktree 17:test.set.worktree 8:worktree + 19:command_parse_error 3:set + 19:command_parse_error 3:set + 19:command_parse_error 3:set + EOF + + test_zformat git config-batch -z >out expect-values <<-EOF && + file:system-config-file system + file:global-config-file global + file:.git/config local with spaces + file:.git/config.worktree worktree + EOF + + git config get --show-origin --regexp --all test.set.* >values && + test_cmp expect-values values +' + +test_expect_success 'unset config by scope and filter' ' + GIT_CONFIG_SYSTEM=system-config-file && + GIT_CONFIG_NOSYSTEM=0 && + GIT_CONFIG_GLOBAL=global-config-file && + export GIT_CONFIG_SYSTEM && + export GIT_CONFIG_NOSYSTEM && + export GIT_CONFIG_GLOBAL && + + cat >in <<-\EOF && + set 1 system test.unset.key system + set 1 global test.unset.key global + set 1 local test.unset.key local with spaces + set 1 worktree test.unset.key worktree + unset 1 system test.unset.key + unset 1 global test.unset.key arg:regex g.* + unset 1 local test.unset.key arg:fixed-value local with spaces + unset 1 worktree test.unset.key arg:fixed-value submodule + unset 1 worktree test.unset.key arg:regex l.* + EOF + + cat >expect <<-\EOF && + set 1 success system test.unset.key system + set 1 success global test.unset.key global + set 1 success local test.unset.key local with spaces + set 1 success worktree test.unset.key worktree + unset 1 success system test.unset.key + unset 1 success global test.unset.key + unset 1 success local test.unset.key + unset 1 failure worktree test.unset.key + unset 1 failure worktree test.unset.key + EOF + + git config-batch out 2>err && + + test_must_be_empty err && + test_cmp expect out && + + cat >expect-values <<-EOF && + file:.git/config.worktree worktree + EOF + + git config get --show-origin --regexp --all test.unset.key >values && + test_cmp expect-values values +' + +test_expect_success 'read/write interactions in sequence' ' + cat >in <<-\EOF && + get 1 local test.rw.missing + set 1 local test.rw.found found + get 1 local test.rw.found + set 1 local test.rw.found updated + get 1 local test.rw.found + unset 1 local test.rw.found arg:fixed-value updated + get 1 local test.rw.found + EOF + + cat >expect <<-\EOF && + get 1 missing test.rw.missing + set 1 success local test.rw.found found + get 1 found test.rw.found local found + set 1 success local test.rw.found updated + get 1 found test.rw.found local updated + unset 1 success local test.rw.found + get 1 missing test.rw.found + EOF + + git config-batch out 2>err && + + test_must_be_empty err && + test_cmp expect out +' + +test_done