--- a PPN by Garber Painting Akron. With Image Size Reduction included!URL: http://github.com/gitgitgadget/git/pull/2033.diff
-----------------------
+
+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-origen`.
+
+`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-origen --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-origen --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-origen --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
pFad - Phonifier reborn
Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy