From 6a41481c6de1cedb059930a710896c5ab2508a12 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 4 Feb 2026 13:23:06 -0800 Subject: [PATCH 01/39] whitespace: symbolic links usually lack LF at the end For a patch that touches a symbolic link, it is perfectly normal that the contents ends with "\ No newline at end of file". The checks introduced recently to detect incomplete lines (i.e., a text file that lack the newline on its final line) should not trigger. Disable the check early for symbolic links, both in "git apply" and "git diff" and test them. For "git apply", we check only when the postimage is a symbolic link regardless of the preimage, and we only care about preimage when applying in reverse. Similarly, "git diff" would warn only when the postimage is a symbolic link, or the preimage when running "git diff -R". Signed-off-by: Junio C Hamano --- apply.c | 20 +++++++++ diff.c | 22 +++++++++- t/t4015-diff-whitespace.sh | 26 ++++++++++++ t/t4124-apply-ws-rule.sh | 86 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/apply.c b/apply.c index c9fb45247d8cd2..f01204d15b4d13 100644 --- a/apply.c +++ b/apply.c @@ -1725,6 +1725,26 @@ static int parse_fragment(struct apply_state *state, unsigned long oldlines, newlines; unsigned long leading, trailing; + /* do not complain a symbolic link being an incomplete line */ + if (patch->ws_rule & WS_INCOMPLETE_LINE) { + /* + * We want to figure out if the postimage is a + * symbolic link when applying the patch normally, or + * if the preimage is a symbolic link when applying + * the patch in reverse. A normal patch only has + * old_mode without new_mode. If it changes the + * filemode, new_mode has value, which is different + * from old_mode. + */ + unsigned mode = (state->apply_in_reverse + ? patch->old_mode + : patch->new_mode + ? patch->new_mode + : patch->old_mode); + if (mode && S_ISLNK(mode)) + patch->ws_rule &= ~WS_INCOMPLETE_LINE; + } + offset = parse_fragment_header(line, len, fragment); if (offset < 0) return -1; diff --git a/diff.c b/diff.c index 7b7cd50dc24351..9e4b92ed69f05d 100644 --- a/diff.c +++ b/diff.c @@ -1834,6 +1834,7 @@ static void emit_rewrite_diff(const char *name_a, const char *a_prefix, *b_prefix; char *data_one, *data_two; size_t size_one, size_two; + unsigned ws_rule; struct emit_callback ecbdata; struct strbuf out = STRBUF_INIT; @@ -1856,9 +1857,15 @@ static void emit_rewrite_diff(const char *name_a, size_one = fill_textconv(o->repo, textconv_one, one, &data_one); size_two = fill_textconv(o->repo, textconv_two, two, &data_two); + ws_rule = whitespace_rule(o->repo->index, name_b); + + /* symlink being an incomplete line is not a news */ + if (DIFF_FILE_VALID(two) && S_ISLNK(two->mode)) + ws_rule &= ~WS_INCOMPLETE_LINE; + memset(&ecbdata, 0, sizeof(ecbdata)); ecbdata.color_diff = o->use_color; - ecbdata.ws_rule = whitespace_rule(o->repo->index, name_b); + ecbdata.ws_rule = ws_rule; ecbdata.opt = o; if (ecbdata.ws_rule & WS_BLANK_AT_EOF) { mmfile_t mf1, mf2; @@ -3762,6 +3769,7 @@ static void builtin_diff(const char *name_a, xpparam_t xpp; xdemitconf_t xecfg; struct emit_callback ecbdata; + unsigned ws_rule; const struct userdiff_funcname *pe; if (must_show_header) { @@ -3773,6 +3781,12 @@ static void builtin_diff(const char *name_a, mf1.size = fill_textconv(o->repo, textconv_one, one, &mf1.ptr); mf2.size = fill_textconv(o->repo, textconv_two, two, &mf2.ptr); + ws_rule = whitespace_rule(o->repo->index, name_b); + + /* symlink being an incomplete line is not a news */ + if (DIFF_FILE_VALID(two) && S_ISLNK(two->mode)) + ws_rule &= ~WS_INCOMPLETE_LINE; + pe = diff_funcname_pattern(o, one); if (!pe) pe = diff_funcname_pattern(o, two); @@ -3784,7 +3798,7 @@ static void builtin_diff(const char *name_a, lbl[0] = NULL; ecbdata.label_path = lbl; ecbdata.color_diff = o->use_color; - ecbdata.ws_rule = whitespace_rule(o->repo->index, name_b); + ecbdata.ws_rule = ws_rule; if (ecbdata.ws_rule & WS_BLANK_AT_EOF) check_blank_at_eof(&mf1, &mf2, &ecbdata); ecbdata.opt = o; @@ -3991,6 +4005,10 @@ static void builtin_checkdiff(const char *name_a, const char *name_b, data.ws_rule = whitespace_rule(o->repo->index, attr_path); data.conflict_marker_size = ll_merge_marker_size(o->repo->index, attr_path); + /* symlink being an incomplete line is not a news */ + if (DIFF_FILE_VALID(two) && S_ISLNK(two->mode)) + data.ws_rule &= ~WS_INCOMPLETE_LINE; + if (fill_mmfile(o->repo, &mf1, one) < 0 || fill_mmfile(o->repo, &mf2, two) < 0) die("unable to read files to diff"); diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh index 3c8eb02e4f3e64..b691d2947943c8 100755 --- a/t/t4015-diff-whitespace.sh +++ b/t/t4015-diff-whitespace.sh @@ -90,6 +90,32 @@ test_expect_success "new incomplete line in post-image" ' git -c core.whitespace=incomplete diff -R --check x ' +test_expect_success SYMLINKS "incomplete-line error is disabled for symlinks" ' + test_when_finished "git reset --hard" && + test_when_finished "rm -f mylink" && + + # a regular file with an incomplete line + printf "%s" one >mylink && + git add mylink && + + # a symbolic link + rm mylink && + ln -s two mylink && + + git -c diff.color=always -c core.whitespace=incomplete \ + diff mylink >forward.raw && + test_decode_color >forward \\\\ No newline at end of file" forward && + + git -c diff.color=always -c core.whitespace=incomplete \ + diff -R mylink >reverse.raw && + test_decode_color >reverse \\\\ No newline at end of file" reverse && + + git -c core.whitespace=incomplete diff --check mylink && + test_must_fail git -c core.whitespace=incomplete diff --check -R mylink +' + test_expect_success "Ray Lehtiniemi's example" ' cat <<-\EOF >x && do { diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh index 115a0f857906d4..29ea7d4268eca4 100755 --- a/t/t4124-apply-ws-rule.sh +++ b/t/t4124-apply-ws-rule.sh @@ -743,4 +743,90 @@ test_expect_success 'incomplete line modified at the end (error)' ' test_cmp sample target ' +test_expect_success "incomplete-line error is disabled for symlinks" ' + test_when_finished "git reset" && + test_when_finished "rm -f patch.txt" && + oneblob=$(printf "one" | git hash-object --stdin -w -t blob) && + twoblob=$(printf "two" | git hash-object --stdin -w -t blob) && + + oneshort=$(git rev-parse --short $oneblob) && + twoshort=$(git rev-parse --short $twoblob) && + + cat >patch0.txt <<-EOF && + diff --git a/mylink b/mylink + index $oneshort..$twoshort 120000 + --- a/mylink + +++ b/mylink + @@ -1 +1 @@ + -one + \ No newline at end of file + +two + \ No newline at end of file + EOF + + # the index has the preimage symlink + git update-index --add --cacheinfo "120000,$oneblob,mylink" && + + # check the patch going forward and reverse + git -c core.whitespace=incomplete apply --cached --check \ + --whitespace=error patch0.txt && + + git update-index --add --cacheinfo "120000,$twoblob,mylink" && + git -c core.whitespace=incomplete apply --cached --check \ + --whitespace=error -R patch0.txt && + + # the patch turns it into the postimage symlink + git update-index --add --cacheinfo "120000,$oneblob,mylink" && + git -c core.whitespace=incomplete apply --cached --whitespace=error \ + patch0.txt && + + # and then back. + git -c core.whitespace=incomplete apply --cached -R --whitespace=error \ + patch0.txt && + + # a text file turns into a symlink + cat >patch1.txt <<-EOF && + diff --git a/mylink b/mylink + deleted file mode 100644 + index $oneshort..0000000 + --- a/mylink + +++ /dev/null + @@ -1 +0,0 @@ + -one + \ No newline at end of file + diff --git a/mylink b/mylink + new file mode 120000 + index 0000000..$twoshort + --- /dev/null + +++ b/mylink + @@ -0,0 +1 @@ + +two + \ No newline at end of file + EOF + + # the index has the preimage text + git update-index --cacheinfo "100644,$oneblob,mylink" && + + # check + git -c core.whitespace=incomplete apply --cached \ + --check --whitespace=error patch1.txt && + + # reverse, leaving an incomplete text file, should error + git update-index --cacheinfo "120000,$twoblob,mylink" && + test_must_fail git -c core.whitespace=incomplete \ + apply --cached --check --whitespace=error -R patch1.txt && + + # apply to create a symbolic link + git update-index --cacheinfo "100644,$oneblob,mylink" && + git -c core.whitespace=incomplete apply --cached --whitespace=error \ + patch1.txt && + + # turning it back into an incomplete text file is an error + test_must_fail git -c core.whitespace=incomplete \ + apply --cached --whitespace=error -R patch1.txt + + + +' + test_done From 2096948158ea0b253a68a81c691fdf88ff00589a Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 29 Jan 2026 11:06:15 -0800 Subject: [PATCH 02/39] checkout: pass program-readable token to unified "main" The "git checkout", "git switch", and "git restore" commands share a single implementation, checkout_main(), which switches error message it gives using the usage string passed by each of these three front-ends. In order to be able to tweak behaviours of the commands based on which one we are executing, invent an enum that denotes which one of these three commands is currently executing, and pass that to checkout_main() instead. With this step, there is no externally visible behaviour change, as this enum parameter is only used to choose among the three usage strings. Signed-off-by: Junio C Hamano --- builtin/checkout.c | 63 +++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index f9453473fe2a20..2411108856d60e 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -43,22 +43,6 @@ #include "parallel-checkout.h" #include "add-interactive.h" -static const char * const checkout_usage[] = { - N_("git checkout [] "), - N_("git checkout [] [] -- ..."), - NULL, -}; - -static const char * const switch_branch_usage[] = { - N_("git switch [] []"), - NULL, -}; - -static const char * const restore_usage[] = { - N_("git restore [] [--source=] ..."), - NULL, -}; - struct checkout_opts { int patch_mode; int patch_context; @@ -1293,6 +1277,13 @@ static void setup_new_branch_info_and_source_tree( } } + +enum checkout_command { + CHECKOUT_CHECKOUT = 1, + CHECKOUT_SWITCH = 2, + CHECKOUT_RESTORE = 3, +}; + static char *parse_remote_branch(const char *arg, struct object_id *rev, int could_be_checkout_paths) @@ -1767,12 +1758,44 @@ static char cb_option = 'b'; static int checkout_main(int argc, const char **argv, const char *prefix, struct checkout_opts *opts, struct option *options, - const char * const usagestr[]) + enum checkout_command which_command) { int parseopt_flags = 0; struct branch_info new_branch_info = { 0 }; int ret; + static const char * const checkout_usage[] = { + N_("git checkout [] "), + N_("git checkout [] [] -- ..."), + NULL, + }; + + static const char * const switch_branch_usage[] = { + N_("git switch [] []"), + NULL, + }; + + static const char * const restore_usage[] = { + N_("git restore [] [--source=] ..."), + NULL, + }; + + const char * const *usagestr; + + switch (which_command) { + case CHECKOUT_CHECKOUT: + usagestr = checkout_usage; + break; + case CHECKOUT_SWITCH: + usagestr = switch_branch_usage; + break; + case CHECKOUT_RESTORE: + usagestr = restore_usage; + break; + default: + BUG("no such checkout variant %d", which_command); + } + opts->overwrite_ignore = 1; opts->prefix = prefix; opts->show_progress = -1; @@ -2032,7 +2055,7 @@ int cmd_checkout(int argc, options = add_checkout_path_options(&opts, options); return checkout_main(argc, argv, prefix, &opts, options, - checkout_usage); + CHECKOUT_CHECKOUT); } int cmd_switch(int argc, @@ -2071,7 +2094,7 @@ int cmd_switch(int argc, cb_option = 'c'; return checkout_main(argc, argv, prefix, &opts, options, - switch_branch_usage); + CHECKOUT_SWITCH); } int cmd_restore(int argc, @@ -2107,5 +2130,5 @@ int cmd_restore(int argc, options = add_checkout_path_options(&opts, options); return checkout_main(argc, argv, prefix, &opts, options, - restore_usage); + CHECKOUT_RESTORE); } From 12fee11f21e9b63626da5058fa055f95d55a5515 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 29 Jan 2026 11:06:16 -0800 Subject: [PATCH 03/39] checkout: tell "parse_remote_branch" which command is calling it When "git checkout " and "git switch " need to error out due to ambiguity of the branch name , these two commands give an advise message with a sample command that tells the user how to disambiguate from the parse_remote_branch() function. The sample command hardcodes "git checkout", since this feature predates "git switch" by a large margin. To a user who said "git switch " and got this message, it is confusing. Pass the "enum checkout_command", which was invented in the previous step for this exact purpose, down the call chain leading to parse_remote_branch() function to change the sample command shown to the user in this advise message. Also add a bit more test coverage for this "fail to DWIM under ambiguity" that we lack, as well as the message we produce when we fail. Reported-by: Simon Cheng Signed-off-by: Junio C Hamano --- builtin/checkout.c | 29 ++++++++++++++++++++++++----- t/t2027-checkout-track.sh | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/builtin/checkout.c b/builtin/checkout.c index 2411108856d60e..f2c5df9631fed0 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1286,7 +1286,8 @@ enum checkout_command { static char *parse_remote_branch(const char *arg, struct object_id *rev, - int could_be_checkout_paths) + int could_be_checkout_paths, + enum checkout_command which_command) { int num_matches = 0; char *remote = unique_tracking_name(arg, rev, &num_matches); @@ -1299,14 +1300,30 @@ static char *parse_remote_branch(const char *arg, if (!remote && num_matches > 1) { if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME)) { + const char *cmdname; + + switch (which_command) { + case CHECKOUT_CHECKOUT: + cmdname = "checkout"; + break; + case CHECKOUT_SWITCH: + cmdname = "switch"; + break; + default: + BUG("command <%d> should not reach parse_remote_branch", + which_command); + break; + } + advise(_("If you meant to check out a remote tracking branch on, e.g. 'origin',\n" "you can do so by fully qualifying the name with the --track option:\n" "\n" - " git checkout --track origin/\n" + " git %s --track origin/\n" "\n" "If you'd like to always have checkouts of an ambiguous prefer\n" "one remote, e.g. the 'origin' remote, consider setting\n" - "checkout.defaultRemote=origin in your config.")); + "checkout.defaultRemote=origin in your config."), + cmdname); } die(_("'%s' matched multiple (%d) remote tracking branches"), @@ -1318,6 +1335,7 @@ static char *parse_remote_branch(const char *arg, static int parse_branchname_arg(int argc, const char **argv, int dwim_new_local_branch_ok, + enum checkout_command which_command, struct branch_info *new_branch_info, struct checkout_opts *opts, struct object_id *rev) @@ -1427,7 +1445,8 @@ static int parse_branchname_arg(int argc, const char **argv, if (recover_with_dwim) { remote = parse_remote_branch(arg, rev, - could_be_checkout_paths); + could_be_checkout_paths, + which_command); if (remote) { *new_branch = arg; arg = remote; @@ -1916,7 +1935,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix, opts->dwim_new_local_branch && opts->track == BRANCH_TRACK_UNSPECIFIED && !opts->new_branch; - int n = parse_branchname_arg(argc, argv, dwim_ok, + int n = parse_branchname_arg(argc, argv, dwim_ok, which_command, &new_branch_info, opts, &rev); argv += n; argc -= n; diff --git a/t/t2027-checkout-track.sh b/t/t2027-checkout-track.sh index a397790df59d0b..c01f1cd617c72b 100755 --- a/t/t2027-checkout-track.sh +++ b/t/t2027-checkout-track.sh @@ -47,4 +47,22 @@ test_expect_success 'checkout --track -b overrides autoSetupMerge=inherit' ' test_cmp_config refs/heads/main branch.b4.merge ' +test_expect_success 'ambiguous tracking info' ' + # Set up a few remote repositories + git init --bare --initial-branch=trunk src1 && + git init --bare --initial-branch=trunk src2 && + git push src1 one:refs/heads/trunk && + git push src2 two:refs/heads/trunk && + + git remote add -f src1 "file://$PWD/src1" && + git remote add -f src2 "file://$PWD/src2" && + + # DWIM + test_must_fail git checkout trunk 2>hint.checkout && + test_grep "hint: *git checkout --track" hint.checkout && + + test_must_fail git switch trunk 2>hint.switch && + test_grep "hint: *git switch --track" hint.switch +' + test_done From ebb667add9b6be123b6eb6f4b9de636c83d6f30c Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Fri, 13 Feb 2026 21:35:15 -0300 Subject: [PATCH 04/39] repo: rename the output format "keyvalue" to "lines" Both subcommands in git-repo(1) accept the "keyvalue" format. This format is newline-delimited, where the key is separated from the value with an equals sign. The name of this option is suboptimal though, as it is both too limiting while at the same time not really indicating what it actually does: - There is no mention of the format being newline-delimited, which is the key differentiator to the "nul" format. - Both "nul" and "keyvalue" have a key and a value, so the latter is not exactly giving any hint what makes it so special. - "keyvalue" requires there to be, well, a key and a value, but we want to add additional output that is only going to be newline delimited. Taken together, "keyvalue" is kind of a bad name for this output format. Luckily, the git-repo(1) command is still rather new and marked as experimental, so things aren't cast into stone yet. Rename the format to "lines" instead to better indicate that the major difference is that we'll get newline-delimited output. This new name will also be a better fit for a subsequent extension in git-repo(1). Helped-by: Patrick Steinhardt Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 21 +++++++++++---------- builtin/repo.c | 19 ++++++++++--------- t/t1900-repo.sh | 4 ++-- t/t1901-repo-structure.sh | 4 ++-- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 7d70270dfa5716..693e1bbced3a5a 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -8,8 +8,8 @@ git-repo - Retrieve information about the repository SYNOPSIS -------- [synopsis] -git repo info [--format=(keyvalue|nul) | -z] [--all | ...] -git repo structure [--format=(table|keyvalue|nul) | -z] +git repo info [--format=(lines|nul) | -z] [--all | ...] +git repo structure [--format=(table|lines|nul) | -z] DESCRIPTION ----------- @@ -19,7 +19,7 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE. COMMANDS -------- -`info [--format=(keyvalue|nul) | -z] [--all | ...]`:: +`info [--format=(lines|nul) | -z] [--all | ...]`:: Retrieve metadata-related information about the current repository. Only the requested data will be returned based on their keys (see "INFO KEYS" section below). @@ -30,21 +30,22 @@ requested. The `--all` flag requests the values for all the available keys. The output format can be chosen through the flag `--format`. Two formats are supported: + -`keyvalue`::: + +`lines`::: output key-value pairs one per line using the `=` character as the delimiter between the key and the value. Values containing "unusual" characters are quoted as explained for the configuration variable `core.quotePath` (see linkgit:git-config[1]). This is the default. `nul`::: - similar to `keyvalue`, but using a newline character as the delimiter + similar to `lines`, but using a newline character as the delimiter between the key and the value and using a NUL character after each value. This format is better suited for being parsed by another applications than - `keyvalue`. Unlike in the `keyvalue` format, the values are never quoted. + `lines`. Unlike in the `lines` format, the values are never quoted. + `-z` is an alias for `--format=nul`. -`structure [--format=(table|keyvalue|nul) | -z]`:: +`structure [--format=(table|lines|nul) | -z]`:: Retrieve statistics about the current repository structure. The following kinds of information are reported: + @@ -61,17 +62,17 @@ supported: change and is not intended for machine parsing. This is the default format. -`keyvalue`::: +`lines`::: Each line of output contains a key-value pair for a repository stat. The '=' character is used to delimit between the key and the value. Values containing "unusual" characters are quoted as explained for the configuration variable `core.quotePath` (see linkgit:git-config[1]). `nul`::: - Similar to `keyvalue`, but uses a NUL character to delimit between + Similar to `lines`, but uses a NUL character to delimit between key-value pairs instead of a newline. Also uses a newline character as the delimiter between the key and value instead of '='. Unlike the - `keyvalue` format, values containing "unusual" characters are never + `lines` format, values containing "unusual" characters are never quoted. + `-z` is an alias for `--format=nul`. diff --git a/builtin/repo.c b/builtin/repo.c index 0ea045abc13f8c..23c5ee88b0bfbd 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -17,8 +17,8 @@ #include "utf8.h" static const char *const repo_usage[] = { - "git repo info [--format=(keyvalue|nul) | -z] [--all | ...]", - "git repo structure [--format=(table|keyvalue|nul) | -z]", + "git repo info [--format=(lines|nul) | -z] [--all | ...]", + "git repo structure [--format=(table|lines|nul) | -z]", NULL }; @@ -26,7 +26,7 @@ typedef int get_value_fn(struct repository *repo, struct strbuf *buf); enum output_format { FORMAT_TABLE, - FORMAT_KEYVALUE, + FORMAT_NEWLINE_TERMINATED, FORMAT_NUL_TERMINATED, }; @@ -91,7 +91,7 @@ static void print_field(enum output_format format, const char *key, const char *value) { switch (format) { - case FORMAT_KEYVALUE: + case FORMAT_NEWLINE_TERMINATED: printf("%s=", key); quote_c_style(value, NULL, stdout, 0); putchar('\n'); @@ -157,8 +157,8 @@ static int parse_format_cb(const struct option *opt, *format = FORMAT_NUL_TERMINATED; else if (!strcmp(arg, "nul")) *format = FORMAT_NUL_TERMINATED; - else if (!strcmp(arg, "keyvalue")) - *format = FORMAT_KEYVALUE; + else if (!strcmp(arg, "lines")) + *format = FORMAT_NEWLINE_TERMINATED; else if (!strcmp(arg, "table")) *format = FORMAT_TABLE; else @@ -170,7 +170,7 @@ static int parse_format_cb(const struct option *opt, static int cmd_repo_info(int argc, const char **argv, const char *prefix, struct repository *repo) { - enum output_format format = FORMAT_KEYVALUE; + enum output_format format = FORMAT_NEWLINE_TERMINATED; int all_keys = 0; struct option options[] = { OPT_CALLBACK_F(0, "format", &format, N_("format"), @@ -185,7 +185,8 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix, }; argc = parse_options(argc, argv, prefix, options, repo_usage, 0); - if (format != FORMAT_KEYVALUE && format != FORMAT_NUL_TERMINATED) + + if (format != FORMAT_NEWLINE_TERMINATED && format != FORMAT_NUL_TERMINATED) die(_("unsupported output format")); if (all_keys && argc) @@ -671,7 +672,7 @@ static int cmd_repo_structure(int argc, const char **argv, const char *prefix, stats_table_setup_structure(&table, &stats); stats_table_print_structure(&table); break; - case FORMAT_KEYVALUE: + case FORMAT_NEWLINE_TERMINATED: structure_keyvalue_print(&stats, '=', '\n'); break; case FORMAT_NUL_TERMINATED: diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh index 51d55f11a5ed66..4155211e5d73e2 100755 --- a/t/t1900-repo.sh +++ b/t/t1900-repo.sh @@ -34,7 +34,7 @@ test_repo_info () { eval "$init_command $repo_name" ' - test_expect_success "keyvalue: $label" ' + test_expect_success "lines: $label" ' echo "$key=$expected_value" > expect && git -C "$repo_name" repo info "$key" >actual && test_cmp expect actual @@ -115,7 +115,7 @@ test_expect_success '-z uses nul-terminated format' ' test_expect_success 'git repo info uses the last requested format' ' echo "layout.bare=false" >expected && - git repo info --format=nul -z --format=keyvalue layout.bare >actual && + git repo info --format=nul -z --format=lines layout.bare >actual && test_cmp expected actual ' diff --git a/t/t1901-repo-structure.sh b/t/t1901-repo-structure.sh index 17ff164b0596c9..a6f2591d9a61ff 100755 --- a/t/t1901-repo-structure.sh +++ b/t/t1901-repo-structure.sh @@ -113,7 +113,7 @@ test_expect_success SHA1 'repository with references and objects' ' ) ' -test_expect_success SHA1 'keyvalue and nul format' ' +test_expect_success SHA1 'lines and nul format' ' test_when_finished "rm -rf repo" && git init repo && ( @@ -140,7 +140,7 @@ test_expect_success SHA1 'keyvalue and nul format' ' objects.tags.disk_size=$(object_type_disk_usage tag) EOF - git repo structure --format=keyvalue >out 2>err && + git repo structure --format=lines >out 2>err && test_cmp expect out && test_line_count = 0 err && From 173c43be54f0039a9f6e13afaad3c953bb6b06dc Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Fri, 13 Feb 2026 21:35:16 -0300 Subject: [PATCH 05/39] repo: add new flag --keys to git-repo-info If the user wants to find what are the available keys, they need to either check the documentation or to ask for all the key-value pairs by using --all. Add a new flag --keys for listing only the available keys without listing the values. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- Documentation/git-repo.adoc | 11 ++++++++++ builtin/repo.c | 32 +++++++++++++++++++++++++++++ t/t1900-repo.sh | 40 +++++++++++++++++++++++++++---------- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/Documentation/git-repo.adoc b/Documentation/git-repo.adoc index 693e1bbced3a5a..319d30bd86e7f4 100644 --- a/Documentation/git-repo.adoc +++ b/Documentation/git-repo.adoc @@ -9,6 +9,7 @@ SYNOPSIS -------- [synopsis] git repo info [--format=(lines|nul) | -z] [--all | ...] +git repo info --keys [--format=(lines|nul) | -z] git repo structure [--format=(table|lines|nul) | -z] DESCRIPTION @@ -45,6 +46,16 @@ supported: + `-z` is an alias for `--format=nul`. +`info --keys [--format=(lines|nul) | -z]`:: + List all the available keys, one per line. The output format can be chosen + through the flag `--format`. The following formats are supported: ++ +`lines`::: + Output the keys one per line. This is the default. + +`nul`::: + Similar to `lines`, but using a _NUL_ character after each value. + `structure [--format=(table|lines|nul) | -z]`:: Retrieve statistics about the current repository structure. The following kinds of information are reported: diff --git a/builtin/repo.c b/builtin/repo.c index 23c5ee88b0bfbd..6a62a6020a5115 100644 --- a/builtin/repo.c +++ b/builtin/repo.c @@ -18,6 +18,7 @@ static const char *const repo_usage[] = { "git repo info [--format=(lines|nul) | -z] [--all | ...]", + "git repo info --keys [--format=(lines|nul) | -z]", "git repo structure [--format=(table|lines|nul) | -z]", NULL }; @@ -148,6 +149,29 @@ static int print_all_fields(struct repository *repo, return 0; } +static int print_keys(enum output_format format) +{ + char sep; + + switch (format) { + case FORMAT_NEWLINE_TERMINATED: + sep = '\n'; + break; + case FORMAT_NUL_TERMINATED: + sep = '\0'; + break; + default: + die(_("--keys can only be used with --format=lines or --format=nul")); + } + + for (size_t i = 0; i < ARRAY_SIZE(repo_info_fields); i++) { + const struct field *field = &repo_info_fields[i]; + printf("%s%c", field->key, sep); + } + + return 0; +} + static int parse_format_cb(const struct option *opt, const char *arg, int unset UNUSED) { @@ -172,6 +196,7 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix, { enum output_format format = FORMAT_NEWLINE_TERMINATED; int all_keys = 0; + int show_keys = 0; struct option options[] = { OPT_CALLBACK_F(0, "format", &format, N_("format"), N_("output format"), @@ -181,11 +206,18 @@ static int cmd_repo_info(int argc, const char **argv, const char *prefix, PARSE_OPT_NONEG | PARSE_OPT_NOARG, parse_format_cb), OPT_BOOL(0, "all", &all_keys, N_("print all keys/values")), + OPT_BOOL(0, "keys", &show_keys, N_("show keys")), OPT_END() }; argc = parse_options(argc, argv, prefix, options, repo_usage, 0); + if (show_keys && (all_keys || argc)) + die(_("--keys cannot be used with a or --all")); + + if (show_keys) + return print_keys(format); + if (format != FORMAT_NEWLINE_TERMINATED && format != FORMAT_NUL_TERMINATED) die(_("unsupported output format")); diff --git a/t/t1900-repo.sh b/t/t1900-repo.sh index 4155211e5d73e2..a9eb07abe8aaea 100755 --- a/t/t1900-repo.sh +++ b/t/t1900-repo.sh @@ -4,15 +4,6 @@ test_description='test git repo-info' . ./test-lib.sh -# git-repo-info keys. It must contain the same keys listed in the const -# repo_info_fields, in lexicographical order. -REPO_INFO_KEYS=' - layout.bare - layout.shallow - object.format - references.format -' - # Test whether a key-value pair is correctly returned # # Usage: test_repo_info