From 19623eb97e4edf76d585100c605037f9e51f6987 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:53 +0530 Subject: [PATCH 01/13] doc: factor out common option In preparation for adding documentation for `git refs list`, factor out the common options from the `git-for-each-ref` man page into a shareable file `for-each-ref-options.adoc` and update `git-for-each-ref.adoc` to use an `include::` macro. This change is a pure refactoring and results in no change to the final rendered documentation for `for-each-ref`. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- Documentation/for-each-ref-options.adoc | 88 ++++++++++++++++++++++++ Documentation/git-for-each-ref.adoc | 89 +------------------------ 2 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 Documentation/for-each-ref-options.adoc diff --git a/Documentation/for-each-ref-options.adoc b/Documentation/for-each-ref-options.adoc new file mode 100644 index 00000000000000..4a033d3e1618db --- /dev/null +++ b/Documentation/for-each-ref-options.adoc @@ -0,0 +1,88 @@ +...:: + If one or more patterns are given, only refs are shown that + match against at least one pattern, either using fnmatch(3) or + literally, in the latter case matching completely or from the + beginning up to a slash. + +--stdin:: + If `--stdin` is supplied, then the list of patterns is read from + standard input instead of from the argument list. + +--count=:: + By default the command shows all refs that match + ``. This option makes it stop after showing + that many refs. + +--sort=:: + A field name to sort on. Prefix `-` to sort in + descending order of the value. When unspecified, + `refname` is used. You may use the --sort= option + multiple times, in which case the last key becomes the primary + key. + +--format=:: + A string that interpolates `%(fieldname)` from a ref being shown and + the object it points at. In addition, the string literal `%%` + renders as `%` and `%xx` - where `xx` are hex digits - renders as + the character with hex code `xx`. For example, `%00` interpolates to + `\0` (NUL), `%09` to `\t` (TAB), and `%0a` to `\n` (LF). ++ +When unspecified, `` defaults to `%(objectname) SPC %(objecttype) +TAB %(refname)`. + +--color[=]:: + Respect any colors specified in the `--format` option. The + `` field must be one of `always`, `never`, or `auto` (if + `` is absent, behave as if `always` was given). + +--shell:: +--perl:: +--python:: +--tcl:: + If given, strings that substitute `%(fieldname)` + placeholders are quoted as string literals suitable for + the specified host language. This is meant to produce + a scriptlet that can directly be `eval`ed. + +--points-at=:: + Only list refs which points at the given object. + +--merged[=]:: + Only list refs whose tips are reachable from the + specified commit (HEAD if not specified). + +--no-merged[=]:: + Only list refs whose tips are not reachable from the + specified commit (HEAD if not specified). + +--contains[=]:: + Only list refs which contain the specified commit (HEAD if not + specified). + +--no-contains[=]:: + Only list refs which don't contain the specified commit (HEAD + if not specified). + +--ignore-case:: + Sorting and filtering refs are case insensitive. + +--omit-empty:: + Do not print a newline after formatted refs where the format expands + to the empty string. + +--exclude=:: + If one or more patterns are given, only refs which do not match + any excluded pattern(s) are shown. Matching is done using the + same rules as `` above. + +--include-root-refs:: + List root refs (HEAD and pseudorefs) apart from regular refs. + +--start-after=:: + Allows paginating the output by skipping references up to and including the + specified marker. When paging, it should be noted that references may be + deleted, modified or added between invocations. Output will only yield those + references which follow the marker lexicographically. Output begins from the + first reference that would come after the marker alphabetically. Cannot be + used with `--sort=` or `--stdin` options, or the __ argument(s) + to limit the refs. diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc index 060940904da21c..130d452de0efe1 100644 --- a/Documentation/git-for-each-ref.adoc +++ b/Documentation/git-for-each-ref.adoc @@ -28,94 +28,7 @@ host language allowing their direct evaluation in that language. OPTIONS ------- -...:: - If one or more patterns are given, only refs are shown that - match against at least one pattern, either using fnmatch(3) or - literally, in the latter case matching completely or from the - beginning up to a slash. - ---stdin:: - If `--stdin` is supplied, then the list of patterns is read from - standard input instead of from the argument list. - ---count=:: - By default the command shows all refs that match - ``. This option makes it stop after showing - that many refs. - ---sort=:: - A field name to sort on. Prefix `-` to sort in - descending order of the value. When unspecified, - `refname` is used. You may use the --sort= option - multiple times, in which case the last key becomes the primary - key. - ---format=:: - A string that interpolates `%(fieldname)` from a ref being shown and - the object it points at. In addition, the string literal `%%` - renders as `%` and `%xx` - where `xx` are hex digits - renders as - the character with hex code `xx`. For example, `%00` interpolates to - `\0` (NUL), `%09` to `\t` (TAB), and `%0a` to `\n` (LF). -+ -When unspecified, `` defaults to `%(objectname) SPC %(objecttype) -TAB %(refname)`. - ---color[=]:: - Respect any colors specified in the `--format` option. The - `` field must be one of `always`, `never`, or `auto` (if - `` is absent, behave as if `always` was given). - ---shell:: ---perl:: ---python:: ---tcl:: - If given, strings that substitute `%(fieldname)` - placeholders are quoted as string literals suitable for - the specified host language. This is meant to produce - a scriptlet that can directly be `eval`ed. - ---points-at=:: - Only list refs which points at the given object. - ---merged[=]:: - Only list refs whose tips are reachable from the - specified commit (HEAD if not specified). - ---no-merged[=]:: - Only list refs whose tips are not reachable from the - specified commit (HEAD if not specified). - ---contains[=]:: - Only list refs which contain the specified commit (HEAD if not - specified). - ---no-contains[=]:: - Only list refs which don't contain the specified commit (HEAD - if not specified). - ---ignore-case:: - Sorting and filtering refs are case insensitive. - ---omit-empty:: - Do not print a newline after formatted refs where the format expands - to the empty string. - ---exclude=:: - If one or more patterns are given, only refs which do not match - any excluded pattern(s) are shown. Matching is done using the - same rules as `` above. - ---include-root-refs:: - List root refs (HEAD and pseudorefs) apart from regular refs. - ---start-after=:: - Allows paginating the output by skipping references up to and including the - specified marker. When paging, it should be noted that references may be - deleted, modified or added between invocations. Output will only yield those - references which follow the marker lexicographically. Output begins from the - first reference that would come after the marker alphabetically. Cannot be - used with `--sort=` or `--stdin` options, or the __ argument(s) - to limit the refs. +include::for-each-ref-options.adoc[] FIELD NAMES ----------- From 69c207dc45c0e95bff2bdcaf1c7aca41e9679fb8 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:54 +0530 Subject: [PATCH 02/13] builtin/for-each-ref: align usage string with the man page Usage string for `git for-each-ref` was out of sync with its official documentation. The test `t0450-txt-doc-vs-help.sh` was marked as broken due to this. Update the usage string to match the documentation. This allows the test to pass, so remove the corresponding 'known breakage' marker from the test file. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- builtin/for-each-ref.c | 15 ++++++++++----- t/t0450/adoc-help-mismatches | 1 - 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index 8b5fe7b65e0105..fe62f078617a83 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -9,12 +9,17 @@ #include "strbuf.h" #include "strvec.h" +#define COMMON_USAGE_FOR_EACH_REF \ + "[--count=] [--shell|--perl|--python|--tcl]\n" \ + " [(--sort=)...] [--format=]\n" \ + " [--include-root-refs] [--points-at=]\n" \ + " [--merged[=]] [--no-merged[=]]\n" \ + " [--contains[=]] [--no-contains[=]]\n" \ + " [(--exclude=)...] [--start-after=]\n" \ + " [ --stdin | ... ]" + static char const * const for_each_ref_usage[] = { - N_("git for-each-ref [] []"), - N_("git for-each-ref [--points-at ]"), - N_("git for-each-ref [--merged []] [--no-merged []]"), - N_("git for-each-ref [--contains []] [--no-contains []]"), - N_("git for-each-ref [--start-after ]"), + "git for-each-ref " COMMON_USAGE_FOR_EACH_REF, NULL }; diff --git a/t/t0450/adoc-help-mismatches b/t/t0450/adoc-help-mismatches index 06b469bdee203d..2c6ecd5fc8e80e 100644 --- a/t/t0450/adoc-help-mismatches +++ b/t/t0450/adoc-help-mismatches @@ -17,7 +17,6 @@ fast-export fast-import fetch-pack fmt-merge-msg -for-each-ref format-patch fsck-objects fsmonitor--daemon From 6eeb1c070a8746734d74064905a8edeae08bd2a8 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:55 +0530 Subject: [PATCH 03/13] builtin/for-each-ref: factor out core logic into a helper The implementation of `git for-each-ref` is monolithic within `cmd_for_each_ref()`, making it impossible to share its logic with other commands. To enable code reuse for the upcoming `git refs list` subcommand, refactor the core logic into a shared helper function. Introduce a new `for-each-ref.h` header to define the public interface for this shared logic. It contains the declaration for a new helper function, `for_each_ref_core()`, and a macro for the common usage options. Move the option parsing, filtering, and formatting logic from `cmd_for_each_ref()` into a new helper function named `for_each_ref_core()`. This helper is made generic by accepting the command's usage string as a parameter. The original `cmd_for_each_ref()` is simplified to a thin wrapper that is only responsible for defining its specific usage array and calling the shared helper. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- builtin/for-each-ref.c | 41 +++++++++++++++++++---------------------- for-each-ref.h | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 for-each-ref.h diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index fe62f078617a83..4af33de57675d8 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -2,6 +2,7 @@ #include "commit.h" #include "config.h" #include "environment.h" +#include "for-each-ref.h" #include "gettext.h" #include "object.h" #include "parse-options.h" @@ -9,24 +10,7 @@ #include "strbuf.h" #include "strvec.h" -#define COMMON_USAGE_FOR_EACH_REF \ - "[--count=] [--shell|--perl|--python|--tcl]\n" \ - " [(--sort=)...] [--format=]\n" \ - " [--include-root-refs] [--points-at=]\n" \ - " [--merged[=]] [--no-merged[=]]\n" \ - " [--contains[=]] [--no-contains[=]]\n" \ - " [(--exclude=)...] [--start-after=]\n" \ - " [ --stdin | ... ]" - -static char const * const for_each_ref_usage[] = { - "git for-each-ref " COMMON_USAGE_FOR_EACH_REF, - NULL -}; - -int cmd_for_each_ref(int argc, - const char **argv, - const char *prefix, - struct repository *repo) +int for_each_ref_core(int argc, const char **argv, const char *prefix, struct repository *repo, const char *const *usage) { struct ref_sorting *sorting; struct string_list sorting_options = STRING_LIST_INIT_DUP; @@ -75,17 +59,17 @@ int cmd_for_each_ref(int argc, /* Set default (refname) sorting */ string_list_append(&sorting_options, "refname"); - parse_options(argc, argv, prefix, opts, for_each_ref_usage, 0); + parse_options(argc, argv, prefix, opts, usage, 0); if (format.array_opts.max_count < 0) { error("invalid --count argument: `%d'", format.array_opts.max_count); - usage_with_options(for_each_ref_usage, opts); + usage_with_options(usage, opts); } if (HAS_MULTI_BITS(format.quote_style)) { error("more than one quoting style?"); - usage_with_options(for_each_ref_usage, opts); + usage_with_options(usage, opts); } if (verify_ref_format(&format)) - usage_with_options(for_each_ref_usage, opts); + usage_with_options(usage, opts); if (filter.start_after && sorting_options.nr > 1) die(_("cannot use --start-after with custom sort options")); @@ -125,3 +109,16 @@ int cmd_for_each_ref(int argc, strvec_clear(&vec); return 0; } + +int cmd_for_each_ref(int argc, + const char **argv, + const char *prefix, + struct repository *repo) +{ + static char const * const for_each_ref_usage[] = { + N_("git for-each-ref " COMMON_USAGE_FOR_EACH_REF), + NULL + }; + + return for_each_ref_core(argc, argv, prefix, repo, for_each_ref_usage); +} diff --git a/for-each-ref.h b/for-each-ref.h new file mode 100644 index 00000000000000..a5e0b6d17aa0d6 --- /dev/null +++ b/for-each-ref.h @@ -0,0 +1,26 @@ +#ifndef FOR_EACH_REF_H +#define FOR_EACH_REF_H + +struct repository; + +/* + * Shared usage string for options common to git-for-each-ref(1) + * and git-refs-list(1). The command-specific part (e.g., "git refs list ") + * must be prepended by the caller. + */ +#define COMMON_USAGE_FOR_EACH_REF \ + "[--count=] [--shell|--perl|--python|--tcl]\n" \ + " [(--sort=)...] [--format=]\n" \ + " [--include-root-refs] [--points-at=]\n" \ + " [--merged[=]] [--no-merged[=]]\n" \ + " [--contains[=]] [--no-contains[=]]\n" \ + " [(--exclude=)...] [--start-after=]\n" \ + " [ --stdin | ... ]" + +/* + * The core logic for for-each-ref and its clones. + */ +int for_each_ref_core(int argc, const char **argv, const char *prefix, + struct repository *repo, const char *const *usage); + +#endif /* FOR_EACH_REF_H */ From eecccfe98bb023a79f3c2b8bc415b6d656d0d381 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:56 +0530 Subject: [PATCH 04/13] builtin/refs: add list subcommand Git's reference management is distributed across multiple commands. As part of an ongoing effort to consolidate and modernize reference handling, introduce a `list` subcommand under the `git refs` umbrella as a replacement for `git for-each-ref`. Implement `cmd_refs_list` by having it call the `for_each_ref_core()` helper function. This helper was factored out of the original `cmd_for_each_ref` in a preceding commit, allowing both commands to share the same core logic as independent peers. Add documentation for the new command. The man page leverages the shared options file, created in a previous commit, by using the AsciiDoc `include::` macro to ensure consistency with git-for-each-ref(1). Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- Documentation/git-refs.adoc | 16 ++++++++++++++++ builtin/refs.c | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc index 4d6dc994f92eb2..e608980711814a 100644 --- a/Documentation/git-refs.adoc +++ b/Documentation/git-refs.adoc @@ -11,6 +11,13 @@ SYNOPSIS [synopsis] git refs migrate --ref-format= [--no-reflog] [--dry-run] git refs verify [--strict] [--verbose] +git refs list [--count=] [--shell|--perl|--python|--tcl] + [(--sort=)...] [--format=] + [--include-root-refs] [--points-at=] + [--merged[=]] [--no-merged[=]] + [--contains[=]] [--no-contains[=]] + [(--exclude=)...] [--start-after=] + [ --stdin | ... ] DESCRIPTION ----------- @@ -26,6 +33,11 @@ migrate:: verify:: Verify reference database consistency. +list:: + List references in the repository with support for filtering, + formatting, and sorting. This subcommand is an alias for + linkgit:git-for-each-ref[1] and offers identical functionality. + OPTIONS ------- @@ -57,6 +69,10 @@ The following options are specific to 'git refs verify': --verbose:: When verifying the reference database consistency, be chatty. +The following options are specific to 'git refs list': + +include::for-each-ref-options.adoc[] + KNOWN LIMITATIONS ----------------- diff --git a/builtin/refs.c b/builtin/refs.c index c7ad0a2963a3d1..76224feba4d55a 100644 --- a/builtin/refs.c +++ b/builtin/refs.c @@ -6,6 +6,7 @@ #include "refs.h" #include "strbuf.h" #include "worktree.h" +#include "for-each-ref.h" #define REFS_MIGRATE_USAGE \ N_("git refs migrate --ref-format= [--no-reflog] [--dry-run]") @@ -101,6 +102,17 @@ static int cmd_refs_verify(int argc, const char **argv, const char *prefix, return ret; } +static int cmd_refs_list(int argc, const char **argv, const char *prefix, + struct repository *repo) +{ + static char const * const refs_list_usage[] = { + N_("git refs list " COMMON_USAGE_FOR_EACH_REF), + NULL + }; + + return for_each_ref_core(argc, argv, prefix, repo, refs_list_usage); +} + int cmd_refs(int argc, const char **argv, const char *prefix, @@ -109,12 +121,14 @@ int cmd_refs(int argc, const char * const refs_usage[] = { REFS_MIGRATE_USAGE, REFS_VERIFY_USAGE, + "git refs list " COMMON_USAGE_FOR_EACH_REF, NULL, }; parse_opt_subcommand_fn *fn = NULL; struct option opts[] = { OPT_SUBCOMMAND("migrate", &fn, cmd_refs_migrate), OPT_SUBCOMMAND("verify", &fn, cmd_refs_verify), + OPT_SUBCOMMAND("list", &fn, cmd_refs_list), OPT_END(), }; From aa91c5c57013bdeca7b58ee5044bf667a4757978 Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:57 +0530 Subject: [PATCH 05/13] t6300: refactor tests to be shareable In preparation for adding tests for the new `git refs list` command, refactor the existing t6300 test suite to make its logic shareable. Move the core test logic from `t6300-for-each-ref.sh` into a new `for-each-ref-tests.sh` file. Inside this new script, replace hardcoded calls to "git for-each-ref" with the `$git_for_each_ref` variable. The original `t6300-for-each-ref.sh` script now becomes a simple "driver". It is responsible for setting the default value of the variable and then sourcing the test library. This new structure follows the established pattern used for sharing tests between `git-blame` and `git-annotate` and prepares the test suite for the `refs list` tests to be added in a subsequent commit. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- t/for-each-ref-tests.sh | 2141 +++++++++++++++++++++++++++++++++++++++ t/t6300-for-each-ref.sh | 2140 +------------------------------------- 2 files changed, 2143 insertions(+), 2138 deletions(-) create mode 100644 t/for-each-ref-tests.sh diff --git a/t/for-each-ref-tests.sh b/t/for-each-ref-tests.sh new file mode 100644 index 00000000000000..e3ad19298accde --- /dev/null +++ b/t/for-each-ref-tests.sh @@ -0,0 +1,2141 @@ +git_for_each_ref=${git_for_each_ref:-git for-each-ref} +GNUPGHOME_NOT_USED=$GNUPGHOME +. "$TEST_DIRECTORY"/lib-gpg.sh +. "$TEST_DIRECTORY"/lib-terminal.sh + +# Mon Jul 3 23:18:43 2006 +0000 +datestamp=1151968723 +setdate_and_increment () { + GIT_COMMITTER_DATE="$datestamp +0200" + datestamp=$(expr "$datestamp" + 1) + GIT_AUTHOR_DATE="$datestamp +0200" + datestamp=$(expr "$datestamp" + 1) + export GIT_COMMITTER_DATE GIT_AUTHOR_DATE +} + +test_object_file_size () { + oid=$(git rev-parse "$1") + path=".git/objects/$(test_oid_to_path $oid)" + test_file_size "$path" +} + +test_expect_success setup ' + # setup .mailmap + cat >.mailmap <<-EOF && + A Thor A U Thor + C Mitter C O Mitter + EOF + + setdate_and_increment && + echo "Using $datestamp" > one && + git add one && + git commit -m "Initial" && + git branch -M main && + setdate_and_increment && + git tag -a -m "Tagging at $datestamp" testtag && + git update-ref refs/remotes/origin/main main && + git remote add origin nowhere && + git config branch.main.remote origin && + git config branch.main.merge refs/heads/main && + git remote add myfork elsewhere && + git config remote.pushdefault myfork && + git config push.default current +' + +test_atom () { + case "$1" in + head) ref=refs/heads/main ;; + tag) ref=refs/tags/testtag ;; + sym) ref=refs/heads/sym ;; + *) ref=$1 ;; + esac + format=$2 + test_do=test_expect_${4:-success} + + printf '%s\n' "$3" >expected + $test_do $PREREQ "basic atom: $ref $format" ' + ${git_for_each_ref} --format="%($format)" "$ref" >actual && + sanitize_pgp actual.clean && + test_cmp expected actual.clean + ' + + # Automatically test "contents:size" atom after testing "contents" + if test "$format" = "contents" + then + # for commit leg, $3 is changed there + expect=$(printf '%s' "$3" | wc -c) + $test_do $PREREQ "basic atom: $ref contents:size" ' + type=$(git cat-file -t "$ref") && + case $type in + tag) + # We cannot use $3 as it expects sanitize_pgp to run + git cat-file tag $ref >out && + expect=$(tail -n +6 out | wc -c) && + rm -f out ;; + tree | blob) + expect="" ;; + commit) + : "use the calculated expect" ;; + *) + BUG "unknown object type" ;; + esac && + # Leave $expect unquoted to lose possible leading whitespaces + echo $expect >expected && + ${git_for_each_ref} --format="%(contents:size)" "$ref" >actual && + test_cmp expected actual + ' + fi +} + +hexlen=$(test_oid hexsz) + +test_atom head refname refs/heads/main +test_atom head refname: refs/heads/main +test_atom head refname:short main +test_atom head refname:lstrip=1 heads/main +test_atom head refname:lstrip=2 main +test_atom head refname:lstrip=-1 main +test_atom head refname:lstrip=-2 heads/main +test_atom head refname:rstrip=1 refs/heads +test_atom head refname:rstrip=2 refs +test_atom head refname:rstrip=-1 refs +test_atom head refname:rstrip=-2 refs/heads +test_atom head refname:strip=1 heads/main +test_atom head refname:strip=2 main +test_atom head refname:strip=-1 main +test_atom head refname:strip=-2 heads/main +test_atom head upstream refs/remotes/origin/main +test_atom head upstream:short origin/main +test_atom head upstream:lstrip=2 origin/main +test_atom head upstream:lstrip=-2 origin/main +test_atom head upstream:rstrip=2 refs/remotes +test_atom head upstream:rstrip=-2 refs/remotes +test_atom head upstream:strip=2 origin/main +test_atom head upstream:strip=-2 origin/main +test_atom head push refs/remotes/myfork/main +test_atom head push:short myfork/main +test_atom head push:lstrip=1 remotes/myfork/main +test_atom head push:lstrip=-1 main +test_atom head push:rstrip=1 refs/remotes/myfork +test_atom head push:rstrip=-1 refs +test_atom head push:strip=1 remotes/myfork/main +test_atom head push:strip=-1 main +test_atom head objecttype commit +test_atom head objectsize $((131 + hexlen)) +test_atom head objectsize:disk $(test_object_file_size refs/heads/main) +test_atom head deltabase $ZERO_OID +test_atom head objectname $(git rev-parse refs/heads/main) +test_atom head objectname:short $(git rev-parse --short refs/heads/main) +test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/main) +test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/main) +test_atom head tree $(git rev-parse refs/heads/main^{tree}) +test_atom head tree:short $(git rev-parse --short refs/heads/main^{tree}) +test_atom head tree:short=1 $(git rev-parse --short=1 refs/heads/main^{tree}) +test_atom head tree:short=10 $(git rev-parse --short=10 refs/heads/main^{tree}) +test_atom head parent '' +test_atom head parent:short '' +test_atom head parent:short=1 '' +test_atom head parent:short=10 '' +test_atom head numparent 0 +test_atom head object '' +test_atom head type '' +test_atom head raw "$(git cat-file commit refs/heads/main) +" +test_atom head '*objectname' '' +test_atom head '*objecttype' '' +test_atom head author 'A U Thor 1151968724 +0200' +test_atom head authorname 'A U Thor' +test_atom head authorname:mailmap 'A Thor' +test_atom head authoremail '' +test_atom head authoremail:trim 'author@example.com' +test_atom head authoremail:localpart 'author' +test_atom head authoremail:trim,localpart 'author' +test_atom head authoremail:mailmap '' +test_atom head authoremail:mailmap,trim 'athor@example.com' +test_atom head authoremail:trim,mailmap 'athor@example.com' +test_atom head authoremail:mailmap,localpart 'athor' +test_atom head authoremail:localpart,mailmap 'athor' +test_atom head authoremail:mailmap,trim,localpart,mailmap,trim 'athor' +test_atom head authordate 'Tue Jul 4 01:18:44 2006 +0200' +test_atom head committer 'C O Mitter 1151968723 +0200' +test_atom head committername 'C O Mitter' +test_atom head committername:mailmap 'C Mitter' +test_atom head committeremail '' +test_atom head committeremail:trim 'committer@example.com' +test_atom head committeremail:localpart 'committer' +test_atom head committeremail:localpart,trim 'committer' +test_atom head committeremail:mailmap '' +test_atom head committeremail:mailmap,trim 'cmitter@example.com' +test_atom head committeremail:trim,mailmap 'cmitter@example.com' +test_atom head committeremail:mailmap,localpart 'cmitter' +test_atom head committeremail:localpart,mailmap 'cmitter' +test_atom head committeremail:trim,mailmap,trim,trim,localpart 'cmitter' +test_atom head committerdate 'Tue Jul 4 01:18:43 2006 +0200' +test_atom head tag '' +test_atom head tagger '' +test_atom head taggername '' +test_atom head taggeremail '' +test_atom head taggeremail:trim '' +test_atom head taggeremail:localpart '' +test_atom head taggerdate '' +test_atom head creator 'C O Mitter 1151968723 +0200' +test_atom head creatordate 'Tue Jul 4 01:18:43 2006 +0200' +test_atom head subject 'Initial' +test_atom head subject:sanitize 'Initial' +test_atom head contents:subject 'Initial' +test_atom head body '' +test_atom head contents:body '' +test_atom head contents:signature '' +test_atom head contents 'Initial +' +test_atom head HEAD '*' + +test_atom tag refname refs/tags/testtag +test_atom tag refname:short testtag +test_atom tag upstream '' +test_atom tag push '' +test_atom tag objecttype tag +test_atom tag objectsize $((114 + hexlen)) +test_atom tag objectsize:disk $(test_object_file_size refs/tags/testtag) +test_atom tag '*objectsize:disk' $(test_object_file_size refs/heads/main) +test_atom tag deltabase $ZERO_OID +test_atom tag '*deltabase' $ZERO_OID +test_atom tag objectname $(git rev-parse refs/tags/testtag) +test_atom tag objectname:short $(git rev-parse --short refs/tags/testtag) +test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/main) +test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/main) +test_atom tag tree '' +test_atom tag tree:short '' +test_atom tag tree:short=1 '' +test_atom tag tree:short=10 '' +test_atom tag parent '' +test_atom tag parent:short '' +test_atom tag parent:short=1 '' +test_atom tag parent:short=10 '' +test_atom tag numparent '' +test_atom tag object $(git rev-parse refs/tags/testtag^0) +test_atom tag type 'commit' +test_atom tag '*objectname' $(git rev-parse refs/tags/testtag^{}) +test_atom tag '*objecttype' 'commit' +test_atom tag author '' +test_atom tag authorname '' +test_atom tag authorname:mailmap '' +test_atom tag authoremail '' +test_atom tag authoremail:trim '' +test_atom tag authoremail:localpart '' +test_atom tag authoremail:trim,localpart '' +test_atom tag authoremail:mailmap '' +test_atom tag authoremail:mailmap,trim '' +test_atom tag authoremail:trim,mailmap '' +test_atom tag authoremail:mailmap,localpart '' +test_atom tag authoremail:localpart,mailmap '' +test_atom tag authoremail:mailmap,trim,localpart,mailmap,trim '' +test_atom tag authordate '' +test_atom tag committer '' +test_atom tag committername '' +test_atom tag committername:mailmap '' +test_atom tag committeremail '' +test_atom tag committeremail:trim '' +test_atom tag committeremail:localpart '' +test_atom tag committeremail:localpart,trim '' +test_atom tag committeremail:mailmap '' +test_atom tag committeremail:mailmap,trim '' +test_atom tag committeremail:trim,mailmap '' +test_atom tag committeremail:mailmap,localpart '' +test_atom tag committeremail:localpart,mailmap '' +test_atom tag committeremail:trim,mailmap,trim,trim,localpart '' +test_atom tag committerdate '' +test_atom tag tag 'testtag' +test_atom tag tagger 'C O Mitter 1151968725 +0200' +test_atom tag taggername 'C O Mitter' +test_atom tag taggername:mailmap 'C Mitter' +test_atom tag taggeremail '' +test_atom tag taggeremail:trim 'committer@example.com' +test_atom tag taggeremail:localpart 'committer' +test_atom tag taggeremail:trim,localpart 'committer' +test_atom tag taggeremail:mailmap '' +test_atom tag taggeremail:mailmap,trim 'cmitter@example.com' +test_atom tag taggeremail:trim,mailmap 'cmitter@example.com' +test_atom tag taggeremail:mailmap,localpart 'cmitter' +test_atom tag taggeremail:localpart,mailmap 'cmitter' +test_atom tag taggeremail:trim,mailmap,trim,localpart,localpart 'cmitter' +test_atom tag taggerdate 'Tue Jul 4 01:18:45 2006 +0200' +test_atom tag creator 'C O Mitter 1151968725 +0200' +test_atom tag creatordate 'Tue Jul 4 01:18:45 2006 +0200' +test_atom tag subject 'Tagging at 1151968727' +test_atom tag subject:sanitize 'Tagging-at-1151968727' +test_atom tag contents:subject 'Tagging at 1151968727' +test_atom tag body '' +test_atom tag contents:body '' +test_atom tag contents:signature '' +test_atom tag contents 'Tagging at 1151968727 +' +test_atom tag HEAD ' ' + +test_expect_success 'basic atom: refs/tags/testtag *raw' ' + git cat-file commit refs/tags/testtag^{} >expected && + ${git_for_each_ref} --format="%(*raw)" refs/tags/testtag >actual && + sanitize_pgp expected.clean && + echo >>expected.clean && + sanitize_pgp actual.clean && + test_cmp expected.clean actual.clean +' + +test_expect_success 'Check invalid atoms names are errors' ' + test_must_fail ${git_for_each_ref} --format="%(INVALID)" refs/heads +' + +test_expect_success 'Check format specifiers are ignored in naming date atoms' ' + ${git_for_each_ref} --format="%(authordate)" refs/heads && + ${git_for_each_ref} --format="%(authordate:default) %(authordate)" refs/heads && + ${git_for_each_ref} --format="%(authordate) %(authordate:default)" refs/heads && + ${git_for_each_ref} --format="%(authordate:default) %(authordate:default)" refs/heads +' + +test_expect_success 'Check valid format specifiers for date fields' ' + ${git_for_each_ref} --format="%(authordate:default)" refs/heads && + ${git_for_each_ref} --format="%(authordate:relative)" refs/heads && + ${git_for_each_ref} --format="%(authordate:short)" refs/heads && + ${git_for_each_ref} --format="%(authordate:local)" refs/heads && + ${git_for_each_ref} --format="%(authordate:iso8601)" refs/heads && + ${git_for_each_ref} --format="%(authordate:rfc2822)" refs/heads +' + +test_expect_success 'Check invalid format specifiers are errors' ' + test_must_fail ${git_for_each_ref} --format="%(authordate:INVALID)" refs/heads +' + +test_expect_success 'arguments to %(objectname:short=) must be positive integers' ' + test_must_fail ${git_for_each_ref} --format="%(objectname:short=0)" && + test_must_fail ${git_for_each_ref} --format="%(objectname:short=-1)" && + test_must_fail ${git_for_each_ref} --format="%(objectname:short=foo)" +' + +test_bad_atom () { + case "$1" in + head) ref=refs/heads/main ;; + tag) ref=refs/tags/testtag ;; + sym) ref=refs/heads/sym ;; + *) ref=$1 ;; + esac + format=$2 + test_do=test_expect_${4:-success} + + printf '%s\n' "$3" >expect + $test_do $PREREQ "err basic atom: $ref $format" ' + test_must_fail ${git_for_each_ref} \ + --format="%($format)" "$ref" 2>error && + test_cmp expect error + ' +} + +test_bad_atom head 'authoremail:foo' \ + 'fatal: unrecognized %(authoremail) argument: foo' + +test_bad_atom head 'authoremail:mailmap,trim,bar' \ + 'fatal: unrecognized %(authoremail) argument: bar' + +test_bad_atom head 'authoremail:trim,' \ + 'fatal: unrecognized %(authoremail) argument: ' + +test_bad_atom head 'authoremail:mailmaptrim' \ + 'fatal: unrecognized %(authoremail) argument: trim' + +test_bad_atom head 'committeremail: ' \ + 'fatal: unrecognized %(committeremail) argument: ' + +test_bad_atom head 'committeremail: trim,foo' \ + 'fatal: unrecognized %(committeremail) argument: trim,foo' + +test_bad_atom head 'committeremail:mailmap,localpart ' \ + 'fatal: unrecognized %(committeremail) argument: ' + +test_bad_atom head 'committeremail:trim_localpart' \ + 'fatal: unrecognized %(committeremail) argument: _localpart' + +test_bad_atom head 'committeremail:localpart,,,trim' \ + 'fatal: unrecognized %(committeremail) argument: ,,trim' + +test_bad_atom tag 'taggeremail:mailmap,trim, foo ' \ + 'fatal: unrecognized %(taggeremail) argument: foo ' + +test_bad_atom tag 'taggeremail:trim,localpart,' \ + 'fatal: unrecognized %(taggeremail) argument: ' + +test_bad_atom tag 'taggeremail:mailmap;localpart trim' \ + 'fatal: unrecognized %(taggeremail) argument: ;localpart trim' + +test_bad_atom tag 'taggeremail:localpart trim' \ + 'fatal: unrecognized %(taggeremail) argument: trim' + +test_bad_atom tag 'taggeremail:mailmap,mailmap,trim,qux,localpart,trim' \ + 'fatal: unrecognized %(taggeremail) argument: qux,localpart,trim' + +test_date () { + f=$1 && + committer_date=$2 && + author_date=$3 && + tagger_date=$4 && + cat >expected <<-EOF && + 'refs/heads/main' '$committer_date' '$author_date' + 'refs/tags/testtag' '$tagger_date' + EOF + ( + ${git_for_each_ref} --shell \ + --format="%(refname) %(committerdate${f:+:$f}) %(authordate${f:+:$f})" \ + refs/heads && + ${git_for_each_ref} --shell \ + --format="%(refname) %(taggerdate${f:+:$f})" \ + refs/tags + ) >actual && + test_cmp expected actual +} + +test_expect_success 'Check unformatted date fields output' ' + test_date "" \ + "Tue Jul 4 01:18:43 2006 +0200" \ + "Tue Jul 4 01:18:44 2006 +0200" \ + "Tue Jul 4 01:18:45 2006 +0200" +' + +test_expect_success 'Check format "default" formatted date fields output' ' + test_date default \ + "Tue Jul 4 01:18:43 2006 +0200" \ + "Tue Jul 4 01:18:44 2006 +0200" \ + "Tue Jul 4 01:18:45 2006 +0200" +' + +test_expect_success 'Check format "default-local" date fields output' ' + test_date default-local "Mon Jul 3 23:18:43 2006" "Mon Jul 3 23:18:44 2006" "Mon Jul 3 23:18:45 2006" +' + +# Don't know how to do relative check because I can't know when this script +# is going to be run and can't fake the current time to git, and hence can't +# provide expected output. Instead, I'll just make sure that "relative" +# doesn't exit in error +test_expect_success 'Check format "relative" date fields output' ' + f=relative && + (${git_for_each_ref} --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads && + ${git_for_each_ref} --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual +' + +# We just check that this is the same as "relative" for now. +test_expect_success 'Check format "relative-local" date fields output' ' + test_date relative-local \ + "$(${git_for_each_ref} --format="%(committerdate:relative)" refs/heads)" \ + "$(${git_for_each_ref} --format="%(authordate:relative)" refs/heads)" \ + "$(${git_for_each_ref} --format="%(taggerdate:relative)" refs/tags)" +' + +test_expect_success 'Check format "short" date fields output' ' + test_date short 2006-07-04 2006-07-04 2006-07-04 +' + +test_expect_success 'Check format "short-local" date fields output' ' + test_date short-local 2006-07-03 2006-07-03 2006-07-03 +' + +test_expect_success 'Check format "local" date fields output' ' + test_date local \ + "Mon Jul 3 23:18:43 2006" \ + "Mon Jul 3 23:18:44 2006" \ + "Mon Jul 3 23:18:45 2006" +' + +test_expect_success 'Check format "iso8601" date fields output' ' + test_date iso8601 \ + "2006-07-04 01:18:43 +0200" \ + "2006-07-04 01:18:44 +0200" \ + "2006-07-04 01:18:45 +0200" +' + +test_expect_success 'Check format "iso8601-local" date fields output' ' + test_date iso8601-local "2006-07-03 23:18:43 +0000" "2006-07-03 23:18:44 +0000" "2006-07-03 23:18:45 +0000" +' + +test_expect_success 'Check format "rfc2822" date fields output' ' + test_date rfc2822 \ + "Tue, 4 Jul 2006 01:18:43 +0200" \ + "Tue, 4 Jul 2006 01:18:44 +0200" \ + "Tue, 4 Jul 2006 01:18:45 +0200" +' + +test_expect_success 'Check format "rfc2822-local" date fields output' ' + test_date rfc2822-local "Mon, 3 Jul 2006 23:18:43 +0000" "Mon, 3 Jul 2006 23:18:44 +0000" "Mon, 3 Jul 2006 23:18:45 +0000" +' + +test_expect_success 'Check format "raw" date fields output' ' + test_date raw "1151968723 +0200" "1151968724 +0200" "1151968725 +0200" +' + +test_expect_success 'Check format "raw-local" date fields output' ' + test_date raw-local "1151968723 +0000" "1151968724 +0000" "1151968725 +0000" +' + +test_expect_success 'Check format of strftime date fields' ' + echo "my date is 2006-07-04" >expected && + ${git_for_each_ref} \ + --format="%(authordate:format:my date is %Y-%m-%d)" \ + refs/heads >actual && + test_cmp expected actual +' + +test_expect_success 'Check format of strftime-local date fields' ' + echo "my date is 2006-07-03" >expected && + ${git_for_each_ref} \ + --format="%(authordate:format-local:my date is %Y-%m-%d)" \ + refs/heads >actual && + test_cmp expected actual +' + +test_expect_success 'exercise strftime with odd fields' ' + echo >expected && + ${git_for_each_ref} --format="%(authordate:format:)" refs/heads >actual && + test_cmp expected actual && + long="long format -- $ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID" && + echo $long >expected && + ${git_for_each_ref} --format="%(authordate:format:$long)" refs/heads >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +refs/heads/main +refs/remotes/origin/main +refs/tags/testtag +EOF + +test_expect_success 'Verify ascending sort' ' + ${git_for_each_ref} --format="%(refname)" --sort=refname >actual && + test_cmp expected actual +' + + +cat >expected <<\EOF +refs/tags/testtag +refs/remotes/origin/main +refs/heads/main +EOF + +test_expect_success 'Verify descending sort' ' + ${git_for_each_ref} --format="%(refname)" --sort=-refname >actual && + test_cmp expected actual +' + +test_expect_success 'Give help even with invalid sort atoms' ' + test_expect_code 129 ${git_for_each_ref} --sort=bogus -h >actual 2>&1 && + grep "^usage: ${git_for_each_ref}" actual +' + +cat >expected <<\EOF +refs/tags/testtag +refs/tags/testtag-2 +EOF + +test_expect_success 'exercise patterns with prefixes' ' + git tag testtag-2 && + test_when_finished "git tag -d testtag-2" && + ${git_for_each_ref} --format="%(refname)" \ + refs/tags/testtag refs/tags/testtag-2 >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +refs/tags/testtag +refs/tags/testtag-2 +EOF + +test_expect_success 'exercise glob patterns with prefixes' ' + git tag testtag-2 && + test_when_finished "git tag -d testtag-2" && + ${git_for_each_ref} --format="%(refname)" \ + refs/tags/testtag "refs/tags/testtag-*" >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +refs/tags/bar +refs/tags/baz +refs/tags/testtag +EOF + +test_expect_success 'exercise patterns with prefix exclusions' ' + for tag in foo/one foo/two foo/three bar baz + do + git tag "$tag" || return 1 + done && + test_when_finished "git tag -d foo/one foo/two foo/three bar baz" && + ${git_for_each_ref} --format="%(refname)" \ + refs/tags/ --exclude=refs/tags/foo >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +refs/tags/bar +refs/tags/baz +refs/tags/foo/one +refs/tags/testtag +EOF + +test_expect_success 'exercise patterns with pattern exclusions' ' + for tag in foo/one foo/two foo/three bar baz + do + git tag "$tag" || return 1 + done && + test_when_finished "git tag -d foo/one foo/two foo/three bar baz" && + ${git_for_each_ref} --format="%(refname)" \ + refs/tags/ --exclude="refs/tags/foo/t*" >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +'refs/heads/main' +'refs/remotes/origin/main' +'refs/tags/testtag' +EOF + +test_expect_success 'Quoting style: shell' ' + ${git_for_each_ref} --shell --format="%(refname)" >actual && + test_cmp expected actual +' + +test_expect_success 'Quoting style: perl' ' + ${git_for_each_ref} --perl --format="%(refname)" >actual && + test_cmp expected actual +' + +test_expect_success 'Quoting style: python' ' + ${git_for_each_ref} --python --format="%(refname)" >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +"refs/heads/main" +"refs/remotes/origin/main" +"refs/tags/testtag" +EOF + +test_expect_success 'Quoting style: tcl' ' + ${git_for_each_ref} --tcl --format="%(refname)" >actual && + test_cmp expected actual +' + +for i in "--perl --shell" "-s --python" "--python --tcl" "--tcl --perl"; do + test_expect_success "more than one quoting style: $i" " + test_must_fail ${git_for_each_ref} $i 2>err && + grep '^error: more than one quoting style' err + " +done + +test_expect_success 'setup for upstream:track[short]' ' + test_commit two +' + +test_atom head upstream:track '[ahead 1]' +test_atom head upstream:trackshort '>' +test_atom head upstream:track,nobracket 'ahead 1' +test_atom head upstream:nobracket,track 'ahead 1' + +test_expect_success 'setup for push:track[short]' ' + test_commit third && + git update-ref refs/remotes/myfork/main main && + git reset main~1 +' + +test_atom head push:track '[behind 1]' +test_atom head push:trackshort '<' + +test_expect_success 'Check that :track[short] cannot be used with other atoms' ' + test_must_fail ${git_for_each_ref} --format="%(refname:track)" 2>/dev/null && + test_must_fail ${git_for_each_ref} --format="%(refname:trackshort)" 2>/dev/null +' + +test_expect_success 'Check that :track[short] works when upstream is invalid' ' + cat >expected <<-\EOF && + [gone] + + EOF + test_when_finished "git config branch.main.merge refs/heads/main" && + git config branch.main.merge refs/heads/does-not-exist && + ${git_for_each_ref} \ + --format="%(upstream:track)$LF%(upstream:trackshort)" \ + refs/heads >actual && + test_cmp expected actual +' + +test_expect_success 'Check for invalid refname format' ' + test_must_fail ${git_for_each_ref} --format="%(refname:INVALID)" +' + +test_expect_success 'set up color tests' ' + cat >expected.color <<-EOF && + $(git rev-parse --short refs/heads/main) main + $(git rev-parse --short refs/remotes/myfork/main) myfork/main + $(git rev-parse --short refs/remotes/origin/main) origin/main + $(git rev-parse --short refs/tags/testtag) testtag + $(git rev-parse --short refs/tags/third) third + $(git rev-parse --short refs/tags/two) two + EOF + sed "s/<[^>]*>//g" expected.bare && + color_format="%(objectname:short) %(color:green)%(refname:short)" +' + +test_expect_success TTY '%(color) shows color with a tty' ' + test_terminal ${git_for_each_ref} --format="$color_format" >actual.raw && + test_decode_color actual && + test_cmp expected.color actual +' + +test_expect_success '%(color) does not show color without tty' ' + TERM=vt100 ${git_for_each_ref} --format="$color_format" >actual && + test_cmp expected.bare actual +' + +test_expect_success '--color can override tty check' ' + ${git_for_each_ref} --color --format="$color_format" >actual.raw && + test_decode_color actual && + test_cmp expected.color actual +' + +test_expect_success 'color.ui=always does not override tty check' ' + git -c color.ui=always ${git_for_each_ref#git} --format="$color_format" >actual && + test_cmp expected.bare actual +' + +test_expect_success 'setup for describe atom tests' ' + git init -b master describe-repo && + ( + cd describe-repo && + + test_commit --no-tag one && + git tag tagone && + + test_commit --no-tag two && + git tag -a -m "tag two" tagtwo + ) +' + +test_expect_success 'describe atom vs git describe' ' + ( + cd describe-repo && + + ${git_for_each_ref} --format="%(objectname)" \ + refs/tags/ >obj && + while read hash + do + if desc=$(git describe $hash) + then + : >expect-contains-good + else + : >expect-contains-bad + fi && + echo "$hash $desc" || return 1 + done expect && + test_path_exists expect-contains-good && + test_path_exists expect-contains-bad && + + ${git_for_each_ref} --format="%(objectname) %(describe)" \ + refs/tags/ >actual 2>err && + test_cmp expect actual && + test_must_be_empty err + ) +' + +test_expect_success 'describe:tags vs describe --tags' ' + ( + cd describe-repo && + git describe --tags >expect && + ${git_for_each_ref} --format="%(describe:tags)" \ + refs/heads/master >actual && + test_cmp expect actual + ) +' + +test_expect_success 'describe:abbrev=... vs describe --abbrev=...' ' + ( + cd describe-repo && + + # Case 1: We have commits between HEAD and the most + # recent tag reachable from it + test_commit --no-tag file && + git describe --abbrev=14 >expect && + ${git_for_each_ref} --format="%(describe:abbrev=14)" \ + refs/heads/master >actual && + test_cmp expect actual && + + # Make sure the hash used is at least 14 digits long + sed -e "s/^.*-g\([0-9a-f]*\)$/\1/" hexpart && + test 15 -le $(wc -c expect && + ${git_for_each_ref} --format="%(describe:abbrev=14)" \ + refs/heads/master >actual && + test_cmp expect actual && + test tagname = $(cat actual) + ) +' + +test_expect_success 'describe:match=... vs describe --match ...' ' + ( + cd describe-repo && + git tag -a -m "tag foo" tag-foo && + git describe --match "*-foo" >expect && + ${git_for_each_ref} --format="%(describe:match="*-foo")" \ + refs/heads/master >actual && + test_cmp expect actual + ) +' + +test_expect_success 'describe:exclude:... vs describe --exclude ...' ' + ( + cd describe-repo && + git tag -a -m "tag bar" tag-bar && + git describe --exclude "*-bar" >expect && + ${git_for_each_ref} --format="%(describe:exclude="*-bar")" \ + refs/heads/master >actual && + test_cmp expect actual + ) +' + +test_expect_success 'deref with describe atom' ' + ( + cd describe-repo && + cat >expect <<-\EOF && + + tagname + tagname + tagname + + tagtwo + EOF + ${git_for_each_ref} --format="%(*describe)" >actual && + test_cmp expect actual + ) +' + +test_expect_success 'err on bad describe atom arg' ' + ( + cd describe-repo && + + # The bad arg is the only arg passed to describe atom + cat >expect <<-\EOF && + fatal: unrecognized %(describe) argument: baz + EOF + test_must_fail ${git_for_each_ref} --format="%(describe:baz)" \ + refs/heads/master 2>actual && + test_cmp expect actual && + + # The bad arg is in the middle of the option string + # passed to the describe atom + cat >expect <<-\EOF && + fatal: unrecognized %(describe) argument: qux=1,abbrev=14 + EOF + test_must_fail ${git_for_each_ref} \ + --format="%(describe:tags,qux=1,abbrev=14)" \ + ref/heads/master 2>actual && + test_cmp expect actual + ) +' + +cat >expected <<\EOF +heads/main +tags/main +EOF + +test_expect_success 'Check ambiguous head and tag refs (strict)' ' + git config --bool core.warnambiguousrefs true && + git checkout -b newtag && + echo "Using $datestamp" > one && + git add one && + git commit -m "Branch" && + setdate_and_increment && + git tag -m "Tagging at $datestamp" main && + ${git_for_each_ref} --format "%(refname:short)" refs/heads/main refs/tags/main >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +heads/main +main +EOF + +test_expect_success 'Check ambiguous head and tag refs (loose)' ' + git config --bool core.warnambiguousrefs false && + ${git_for_each_ref} --format "%(refname:short)" refs/heads/main refs/tags/main >actual && + test_cmp expected actual +' + +cat >expected <<\EOF +heads/ambiguous +ambiguous +EOF + +test_expect_success 'Check ambiguous head and tag refs II (loose)' ' + git checkout main && + git tag ambiguous testtag^0 && + git branch ambiguous testtag^0 && + ${git_for_each_ref} --format "%(refname:short)" refs/heads/ambiguous refs/tags/ambiguous >actual && + test_cmp expected actual +' + +test_expect_success 'create tag without tagger' ' + git tag -a -m "Broken tag" taggerless && + git tag -f taggerless $(git cat-file tag taggerless | + sed -e "/^tagger /d" | + git hash-object --literally --stdin -w -t tag) +' + +test_atom refs/tags/taggerless type 'commit' +test_atom refs/tags/taggerless tag 'taggerless' +test_atom refs/tags/taggerless tagger '' +test_atom refs/tags/taggerless taggername '' +test_atom refs/tags/taggerless taggeremail '' +test_atom refs/tags/taggerless taggeremail:trim '' +test_atom refs/tags/taggerless taggeremail:localpart '' +test_atom refs/tags/taggerless taggerdate '' +test_atom refs/tags/taggerless committer '' +test_atom refs/tags/taggerless committername '' +test_atom refs/tags/taggerless committeremail '' +test_atom refs/tags/taggerless committeremail:trim '' +test_atom refs/tags/taggerless committeremail:localpart '' +test_atom refs/tags/taggerless committerdate '' +test_atom refs/tags/taggerless subject 'Broken tag' + +test_expect_success 'an unusual tag with an incomplete line' ' + + git tag -m "bogo" bogo && + bogo=$(git cat-file tag bogo) && + bogo=$(printf "%s" "$bogo" | git mktag) && + git tag -f bogo "$bogo" && + ${git_for_each_ref} --format "%(body)" refs/tags/bogo + +' + +test_expect_success 'create tag with subject and body content' ' + cat >>msg <<-\EOF && + the subject line + + first body line + second body line + EOF + git tag -F msg subject-body +' +test_atom refs/tags/subject-body subject 'the subject line' +test_atom refs/tags/subject-body subject:sanitize 'the-subject-line' +test_atom refs/tags/subject-body body 'first body line +second body line +' +test_atom refs/tags/subject-body contents 'the subject line + +first body line +second body line +' + +test_expect_success 'create tag with multiline subject' ' + cat >msg <<-\EOF && + first subject line + second subject line + + first body line + second body line + EOF + git tag -F msg multiline +' +test_atom refs/tags/multiline subject 'first subject line second subject line' +test_atom refs/tags/multiline subject:sanitize 'first-subject-line-second-subject-line' +test_atom refs/tags/multiline contents:subject 'first subject line second subject line' +test_atom refs/tags/multiline body 'first body line +second body line +' +test_atom refs/tags/multiline contents:body 'first body line +second body line +' +test_atom refs/tags/multiline contents:signature '' +test_atom refs/tags/multiline contents 'first subject line +second subject line + +first body line +second body line +' + +test_expect_success GPG 'create signed tags' ' + git tag -s -m "" signed-empty && + git tag -s -m "subject line" signed-short && + cat >msg <<-\EOF && + subject line + + body contents + EOF + git tag -s -F msg signed-long +' + +sig='-----BEGIN PGP SIGNATURE----- +-----END PGP SIGNATURE----- +' + +PREREQ=GPG +test_atom refs/tags/signed-empty subject '' +test_atom refs/tags/signed-empty subject:sanitize '' +test_atom refs/tags/signed-empty contents:subject '' +test_atom refs/tags/signed-empty body "$sig" +test_atom refs/tags/signed-empty contents:body '' +test_atom refs/tags/signed-empty contents:signature "$sig" +test_atom refs/tags/signed-empty contents "$sig" + +test_expect_success GPG 'basic atom: refs/tags/signed-empty raw' ' + git cat-file tag refs/tags/signed-empty >expected && + ${git_for_each_ref} --format="%(raw)" refs/tags/signed-empty >actual && + sanitize_pgp expected.clean && + echo >>expected.clean && + sanitize_pgp actual.clean && + test_cmp expected.clean actual.clean +' + +test_atom refs/tags/signed-short subject 'subject line' +test_atom refs/tags/signed-short subject:sanitize 'subject-line' +test_atom refs/tags/signed-short contents:subject 'subject line' +test_atom refs/tags/signed-short body "$sig" +test_atom refs/tags/signed-short contents:body '' +test_atom refs/tags/signed-short contents:signature "$sig" +test_atom refs/tags/signed-short contents "subject line +$sig" + +test_expect_success GPG 'basic atom: refs/tags/signed-short raw' ' + git cat-file tag refs/tags/signed-short >expected && + ${git_for_each_ref} --format="%(raw)" refs/tags/signed-short >actual && + sanitize_pgp expected.clean && + echo >>expected.clean && + sanitize_pgp actual.clean && + test_cmp expected.clean actual.clean +' + +test_atom refs/tags/signed-long subject 'subject line' +test_atom refs/tags/signed-long subject:sanitize 'subject-line' +test_atom refs/tags/signed-long contents:subject 'subject line' +test_atom refs/tags/signed-long body "body contents +$sig" +test_atom refs/tags/signed-long contents:body 'body contents +' +test_atom refs/tags/signed-long contents:signature "$sig" +test_atom refs/tags/signed-long contents "subject line + +body contents +$sig" + +test_expect_success GPG 'basic atom: refs/tags/signed-long raw' ' + git cat-file tag refs/tags/signed-long >expected && + ${git_for_each_ref} --format="%(raw)" refs/tags/signed-long >actual && + sanitize_pgp expected.clean && + echo >>expected.clean && + sanitize_pgp actual.clean && + test_cmp expected.clean actual.clean +' + +test_expect_success 'set up refs pointing to tree and blob' ' + git update-ref refs/mytrees/first refs/heads/main^{tree} && + git update-ref refs/myblobs/first refs/heads/main:one +' + +test_atom refs/mytrees/first subject "" +test_atom refs/mytrees/first contents:subject "" +test_atom refs/mytrees/first body "" +test_atom refs/mytrees/first contents:body "" +test_atom refs/mytrees/first contents:signature "" +test_atom refs/mytrees/first contents "" + +test_expect_success 'basic atom: refs/mytrees/first raw' ' + git cat-file tree refs/mytrees/first >expected && + echo >>expected && + ${git_for_each_ref} --format="%(raw)" refs/mytrees/first >actual && + test_cmp expected actual && + git cat-file -s refs/mytrees/first >expected && + ${git_for_each_ref} --format="%(raw:size)" refs/mytrees/first >actual && + test_cmp expected actual +' + +test_atom refs/myblobs/first subject "" +test_atom refs/myblobs/first contents:subject "" +test_atom refs/myblobs/first body "" +test_atom refs/myblobs/first contents:body "" +test_atom refs/myblobs/first contents:signature "" +test_atom refs/myblobs/first contents "" + +test_expect_success 'basic atom: refs/myblobs/first raw' ' + git cat-file blob refs/myblobs/first >expected && + echo >>expected && + ${git_for_each_ref} --format="%(raw)" refs/myblobs/first >actual && + test_cmp expected actual && + git cat-file -s refs/myblobs/first >expected && + ${git_for_each_ref} --format="%(raw:size)" refs/myblobs/first >actual && + test_cmp expected actual +' + +test_expect_success 'set up refs pointing to binary blob' ' + printf "a\0b\0c" >blob1 && + printf "a\0c\0b" >blob2 && + printf "\0a\0b\0c" >blob3 && + printf "abc" >blob4 && + printf "\0 \0 \0 " >blob5 && + printf "\0 \0a\0 " >blob6 && + printf " " >blob7 && + >blob8 && + obj=$(git hash-object -w blob1) && + git update-ref refs/myblobs/blob1 "$obj" && + obj=$(git hash-object -w blob2) && + git update-ref refs/myblobs/blob2 "$obj" && + obj=$(git hash-object -w blob3) && + git update-ref refs/myblobs/blob3 "$obj" && + obj=$(git hash-object -w blob4) && + git update-ref refs/myblobs/blob4 "$obj" && + obj=$(git hash-object -w blob5) && + git update-ref refs/myblobs/blob5 "$obj" && + obj=$(git hash-object -w blob6) && + git update-ref refs/myblobs/blob6 "$obj" && + obj=$(git hash-object -w blob7) && + git update-ref refs/myblobs/blob7 "$obj" && + obj=$(git hash-object -w blob8) && + git update-ref refs/myblobs/blob8 "$obj" +' + +test_expect_success 'Verify sorts with raw' ' + cat >expected <<-EOF && + refs/myblobs/blob8 + refs/myblobs/blob5 + refs/myblobs/blob6 + refs/myblobs/blob3 + refs/myblobs/blob7 + refs/mytrees/first + refs/myblobs/first + refs/myblobs/blob1 + refs/myblobs/blob2 + refs/myblobs/blob4 + refs/heads/main + EOF + ${git_for_each_ref} --format="%(refname)" --sort=raw \ + refs/heads/main refs/myblobs/ refs/mytrees/first >actual && + test_cmp expected actual +' + +test_expect_success 'Verify sorts with raw:size' ' + cat >expected <<-EOF && + refs/myblobs/blob8 + refs/myblobs/blob7 + refs/myblobs/blob4 + refs/myblobs/blob1 + refs/myblobs/blob2 + refs/myblobs/blob3 + refs/myblobs/blob5 + refs/myblobs/blob6 + refs/myblobs/first + refs/mytrees/first + refs/heads/main + EOF + ${git_for_each_ref} --format="%(refname)" --sort=raw:size \ + refs/heads/main refs/myblobs/ refs/mytrees/first >actual && + test_cmp expected actual +' + +test_expect_success 'validate raw atom with %(if:equals)' ' + cat >expected <<-EOF && + not equals + not equals + not equals + not equals + not equals + not equals + refs/myblobs/blob4 + not equals + not equals + not equals + not equals + not equals + EOF + ${git_for_each_ref} --format="%(if:equals=abc)%(raw)%(then)%(refname)%(else)not equals%(end)" \ + refs/myblobs/ refs/heads/ >actual && + test_cmp expected actual +' + +test_expect_success 'validate raw atom with %(if:notequals)' ' + cat >expected <<-EOF && + refs/heads/ambiguous + refs/heads/main + refs/heads/newtag + refs/myblobs/blob1 + refs/myblobs/blob2 + refs/myblobs/blob3 + equals + refs/myblobs/blob5 + refs/myblobs/blob6 + refs/myblobs/blob7 + refs/myblobs/blob8 + refs/myblobs/first + EOF + ${git_for_each_ref} --format="%(if:notequals=abc)%(raw)%(then)%(refname)%(else)equals%(end)" \ + refs/myblobs/ refs/heads/ >actual && + test_cmp expected actual +' + +test_expect_success 'empty raw refs with %(if)' ' + cat >expected <<-EOF && + refs/myblobs/blob1 not empty + refs/myblobs/blob2 not empty + refs/myblobs/blob3 not empty + refs/myblobs/blob4 not empty + refs/myblobs/blob5 not empty + refs/myblobs/blob6 not empty + refs/myblobs/blob7 empty + refs/myblobs/blob8 empty + refs/myblobs/first not empty + EOF + ${git_for_each_ref} --format="%(refname) %(if)%(raw)%(then)not empty%(else)empty%(end)" \ + refs/myblobs/ >actual && + test_cmp expected actual +' + +test_expect_success '%(raw) with --python must fail' ' + test_must_fail ${git_for_each_ref} --format="%(raw)" --python +' + +test_expect_success '%(raw) with --tcl must fail' ' + test_must_fail ${git_for_each_ref} --format="%(raw)" --tcl +' + +test_expect_success PERL_TEST_HELPERS '%(raw) with --perl' ' + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/myblobs/blob1 --perl | perl >actual && + cmp blob1 actual && + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/myblobs/blob3 --perl | perl >actual && + cmp blob3 actual && + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/myblobs/blob8 --perl | perl >actual && + cmp blob8 actual && + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/myblobs/first --perl | perl >actual && + cmp one actual && + git cat-file tree refs/mytrees/first > expected && + ${git_for_each_ref} --format="\$name= %(raw); +print \"\$name\"" refs/mytrees/first --perl | perl >actual && + cmp expected actual +' + +test_expect_success '%(raw) with --shell must fail' ' + test_must_fail ${git_for_each_ref} --format="%(raw)" --shell +' + +test_expect_success '%(raw) with --shell and --sort=raw must fail' ' + test_must_fail ${git_for_each_ref} --format="%(raw)" --sort=raw --shell +' + +test_expect_success '%(raw:size) with --shell' ' + ${git_for_each_ref} --format="%(raw:size)" | sed "s/^/$SQ/;s/$/$SQ/" >expect && + ${git_for_each_ref} --format="%(raw:size)" --shell >actual && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} --format compare with cat-file --batch" ' + git rev-parse refs/mytrees/first | git cat-file --batch >expected && + ${git_for_each_ref} --format="%(objectname) %(objecttype) %(objectsize) +%(raw)" refs/mytrees/first >actual && + test_cmp expected actual +' + +test_expect_success 'verify sorts with contents:size' ' + cat >expect <<-\EOF && + refs/heads/main + refs/heads/newtag + refs/heads/ambiguous + EOF + ${git_for_each_ref} --format="%(refname)" \ + --sort=contents:size refs/heads/ >actual && + test_cmp expect actual +' + +test_expect_success 'set up multiple-sort tags' ' + for when in 100000 200000 + do + for email in user1 user2 + do + for ref in ref1 ref2 + do + GIT_COMMITTER_DATE="@$when +0000" \ + GIT_COMMITTER_EMAIL="$email@example.com" \ + git tag -m "tag $ref-$when-$email" \ + multi-$ref-$when-$email || return 1 + done + done + done +' + +test_expect_success 'Verify sort with multiple keys' ' + cat >expected <<-\EOF && + 100000 refs/tags/multi-ref2-100000-user1 + 100000 refs/tags/multi-ref1-100000-user1 + 100000 refs/tags/multi-ref2-100000-user2 + 100000 refs/tags/multi-ref1-100000-user2 + 200000 refs/tags/multi-ref2-200000-user1 + 200000 refs/tags/multi-ref1-200000-user1 + 200000 refs/tags/multi-ref2-200000-user2 + 200000 refs/tags/multi-ref1-200000-user2 + EOF + ${git_for_each_ref} \ + --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ + --sort=-refname \ + --sort=taggeremail \ + --sort=taggerdate \ + "refs/tags/multi-*" >actual && + test_cmp expected actual +' + +test_expect_success 'equivalent sorts fall back on refname' ' + cat >expected <<-\EOF && + 100000 refs/tags/multi-ref1-100000-user1 + 100000 refs/tags/multi-ref1-100000-user2 + 100000 refs/tags/multi-ref2-100000-user1 + 100000 refs/tags/multi-ref2-100000-user2 + 200000 refs/tags/multi-ref1-200000-user1 + 200000 refs/tags/multi-ref1-200000-user2 + 200000 refs/tags/multi-ref2-200000-user1 + 200000 refs/tags/multi-ref2-200000-user2 + EOF + ${git_for_each_ref} \ + --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ + --sort=taggerdate \ + "refs/tags/multi-*" >actual && + test_cmp expected actual +' + +test_expect_success '--no-sort cancels the previous sort keys' ' + cat >expected <<-\EOF && + 100000 refs/tags/multi-ref1-100000-user1 + 100000 refs/tags/multi-ref1-100000-user2 + 100000 refs/tags/multi-ref2-100000-user1 + 100000 refs/tags/multi-ref2-100000-user2 + 200000 refs/tags/multi-ref1-200000-user1 + 200000 refs/tags/multi-ref1-200000-user2 + 200000 refs/tags/multi-ref2-200000-user1 + 200000 refs/tags/multi-ref2-200000-user2 + EOF + ${git_for_each_ref} \ + --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ + --sort=-refname \ + --sort=taggeremail \ + --no-sort \ + --sort=taggerdate \ + "refs/tags/multi-*" >actual && + test_cmp expected actual +' + +test_expect_success '--no-sort without subsequent --sort prints expected refs' ' + cat >expected <<-\EOF && + refs/tags/multi-ref1-100000-user1 + refs/tags/multi-ref1-100000-user2 + refs/tags/multi-ref1-200000-user1 + refs/tags/multi-ref1-200000-user2 + refs/tags/multi-ref2-100000-user1 + refs/tags/multi-ref2-100000-user2 + refs/tags/multi-ref2-200000-user1 + refs/tags/multi-ref2-200000-user2 + EOF + + # Sort the results with `sort` for a consistent comparison against + # expected + ${git_for_each_ref} \ + --format="%(refname)" \ + --no-sort \ + "refs/tags/multi-*" | sort >actual && + test_cmp expected actual +' + +test_expect_success 'set up custom date sorting' ' + # Dates: + # - Wed Feb 07 2024 21:34:20 +0000 + # - Tue Dec 14 1999 00:05:22 +0000 + # - Fri Jun 04 2021 11:26:51 +0000 + # - Mon Jan 22 2007 16:44:01 GMT+0000 + i=1 && + for when in 1707341660 945129922 1622806011 1169484241 + do + GIT_COMMITTER_DATE="@$when +0000" \ + GIT_COMMITTER_EMAIL="user@example.com" \ + git tag -m "tag $when" custom-dates-$i && + i=$(($i+1)) || return 1 + done +' + +test_expect_success 'sort by date defaults to full timestamp' ' + cat >expected <<-\EOF && + 945129922 refs/tags/custom-dates-2 + 1169484241 refs/tags/custom-dates-4 + 1622806011 refs/tags/custom-dates-3 + 1707341660 refs/tags/custom-dates-1 + EOF + + ${git_for_each_ref} \ + --format="%(creatordate:unix) %(refname)" \ + --sort=creatordate \ + "refs/tags/custom-dates-*" >actual && + test_cmp expected actual +' + +test_expect_success 'sort by custom date format' ' + cat >expected <<-\EOF && + 00:05:22 refs/tags/custom-dates-2 + 11:26:51 refs/tags/custom-dates-3 + 16:44:01 refs/tags/custom-dates-4 + 21:34:20 refs/tags/custom-dates-1 + EOF + + ${git_for_each_ref} \ + --format="%(creatordate:format:%H:%M:%S) %(refname)" \ + --sort="creatordate:format:%H:%M:%S" \ + "refs/tags/custom-dates-*" >actual && + test_cmp expected actual +' + +test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' ' + test_when_finished "git checkout main" && + ${git_for_each_ref} --format="%(HEAD) %(refname:short)" refs/heads/ >actual && + sed -e "s/^\* / /" actual >expect && + git checkout --orphan orphaned-branch && + ${git_for_each_ref} --format="%(HEAD) %(refname:short)" refs/heads/ >actual && + test_cmp expect actual +' + +cat >trailers < +Signed-off-by: A U Thor +[ v2 updated patch description ] +Acked-by: A U Thor + +EOF + +unfold () { + perl -0pe 's/\n\s+/ /g' +} + +test_expect_success 'set up trailers for next test' ' + echo "Some contents" > two && + git add two && + git commit -F - <<-EOF + trailers: this commit message has trailers + + Some message contents + + $(cat trailers) + EOF +' + +test_trailer_option () { + if test "$#" -eq 3 + then + prereq="$1" + shift + fi && + title=$1 option=$2 + cat >expect + test_expect_success $prereq "$title" ' + ${git_for_each_ref} --format="%($option)" refs/heads/main >actual && + test_cmp expect actual && + ${git_for_each_ref} --format="%(contents:$option)" refs/heads/main >actual && + test_cmp expect actual + ' +} + +test_trailer_option PERL_TEST_HELPERS '%(trailers:unfold) unfolds trailers' \ + 'trailers:unfold' <<-EOF + $(unfold + + EOF + +test_trailer_option '%(trailers:key=foo) is case insensitive' \ + 'trailers:key=SiGned-oFf-bY' <<-EOF + Signed-off-by: A U Thor + + EOF + +test_trailer_option '%(trailers:key=foo:) trailing colon also works' \ + 'trailers:key=Signed-off-by:' <<-EOF + Signed-off-by: A U Thor + + EOF + +test_trailer_option '%(trailers:key=foo) multiple keys' \ + 'trailers:key=Reviewed-by:,key=Signed-off-by' <<-EOF + Reviewed-by: A U Thor + Signed-off-by: A U Thor + + EOF + +test_trailer_option '%(trailers:key=nonexistent) becomes empty' \ + 'trailers:key=Shined-off-by:' <<-EOF + + EOF + +test_trailer_option '%(trailers:key=foo) handles multiple lines even if folded' \ + 'trailers:key=Acked-by' <<-EOF + $(grep -v patch.description + $(grep patch.description + + EOF + +test_trailer_option '%(trailers:separator) changes separator' \ + 'trailers:separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF + Reviewed-by: A U Thor ,Signed-off-by: A U Thor + EOF + +test_trailer_option '%(trailers:key_value_separator) changes key-value separator' \ + 'trailers:key_value_separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF + Reviewed-by,A U Thor + Signed-off-by,A U Thor + + EOF + +test_trailer_option '%(trailers:separator,key_value_separator) changes both separators' \ + 'trailers:separator=%x2C,key_value_separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF + Reviewed-by,A U Thor ,Signed-off-by,A U Thor + EOF + +test_expect_success 'multiple %(trailers) use their own options' ' + git tag -F - tag-with-trailers <<-\EOF && + body + + one: foo + one: bar + two: baz + two: qux + EOF + t1="%(trailers:key=one,key_value_separator=W,separator=X)" && + t2="%(trailers:key=two,key_value_separator=Y,separator=Z)" && + ${git_for_each_ref} --format="$t1%0a$t2" refs/tags/tag-with-trailers >actual && + cat >expect <<-\EOF && + oneWfooXoneWbar + twoYbazZtwoYqux + EOF + test_cmp expect actual +' + +test_failing_trailer_option () { + title=$1 option=$2 + cat >expect + test_expect_success "$title" ' + # error message cannot be checked under i18n + test_must_fail ${git_for_each_ref} --format="%($option)" refs/heads/main 2>actual && + test_cmp expect actual && + test_must_fail ${git_for_each_ref} --format="%(contents:$option)" refs/heads/main 2>actual && + test_cmp expect actual + ' +} + +test_failing_trailer_option '%(trailers) rejects unknown trailers arguments' \ + 'trailers:unsupported' <<-\EOF + fatal: unknown %(trailers) argument: unsupported + EOF + +test_failing_trailer_option '%(trailers:key) without value is error' \ + 'trailers:key' <<-\EOF + fatal: expected %(trailers:key=) + EOF + +test_expect_success 'if arguments, %(contents:trailers) shows error if colon is missing' ' + cat >expect <<-EOF && + fatal: unrecognized %(contents) argument: trailersonly + EOF + test_must_fail ${git_for_each_ref} --format="%(contents:trailersonly)" 2>actual && + test_cmp expect actual +' + +test_expect_success 'basic atom: head contents:trailers' ' + ${git_for_each_ref} --format="%(contents:trailers)" refs/heads/main >actual && + sanitize_pgp actual.clean && + # ${git_for_each_ref} ends with a blank line + cat >expect <<-EOF && + $(cat trailers) + + EOF + test_cmp expect actual.clean +' + +test_expect_success 'basic atom: rest must fail' ' + test_must_fail ${git_for_each_ref} --format="%(rest)" refs/heads/main +' + +test_expect_success 'HEAD atom does not take arguments' ' + test_must_fail ${git_for_each_ref} --format="%(HEAD:foo)" 2>err && + echo "fatal: %(HEAD) does not take arguments" >expect && + test_cmp expect err +' + +test_expect_success 'subject atom rejects unknown arguments' ' + test_must_fail ${git_for_each_ref} --format="%(subject:foo)" 2>err && + echo "fatal: unrecognized %(subject) argument: foo" >expect && + test_cmp expect err +' + +test_expect_success 'refname atom rejects unknown arguments' ' + test_must_fail ${git_for_each_ref} --format="%(refname:foo)" 2>err && + echo "fatal: unrecognized %(refname) argument: foo" >expect && + test_cmp expect err +' + +test_expect_success 'trailer parsing not fooled by --- line' ' + git commit --allow-empty -F - <<-\EOF && + this is the subject + + This is the body. The message has a "---" line which would confuse a + message+patch parser. But here we know we have only a commit message, + so we get it right. + + trailer: wrong + --- + This is more body. + + trailer: right + EOF + + { + echo "trailer: right" && + echo + } >expect && + ${git_for_each_ref} --format="%(trailers)" refs/heads/main >actual && + test_cmp expect actual +' + +test_expect_success 'Add symbolic ref for the following tests' ' + git symbolic-ref refs/heads/sym refs/heads/main +' + +cat >expected <actual && + test_cmp expected actual +' + +cat >expected <actual && + test_cmp expected actual +' + +cat >expected < actual && + ${git_for_each_ref} --format="%(symref:lstrip=-2)" refs/heads/sym >> actual && + test_cmp expected actual && + + ${git_for_each_ref} --format="%(symref:strip=2)" refs/heads/sym > actual && + ${git_for_each_ref} --format="%(symref:strip=-2)" refs/heads/sym >> actual && + test_cmp expected actual +' + +cat >expected < actual && + ${git_for_each_ref} --format="%(symref:rstrip=-2)" refs/heads/sym >> actual && + test_cmp expected actual +' + +test_expect_success ':remotename and :remoteref' ' + git init remote-tests && + ( + cd remote-tests && + test_commit initial && + git branch -M main && + git remote add from fifth.coffee:blub && + git config branch.main.remote from && + git config branch.main.merge refs/heads/stable && + git remote add to southridge.audio:repo && + git config remote.to.push "refs/heads/*:refs/heads/pushed/*" && + git config branch.main.pushRemote to && + for pair in "%(upstream)=refs/remotes/from/stable" \ + "%(upstream:remotename)=from" \ + "%(upstream:remoteref)=refs/heads/stable" \ + "%(push)=refs/remotes/to/pushed/main" \ + "%(push:remotename)=to" \ + "%(push:remoteref)=refs/heads/pushed/main" + do + echo "${pair#*=}" >expect && + ${git_for_each_ref} --format="${pair%=*}" \ + refs/heads/main >actual && + test_cmp expect actual || exit 1 + done && + git branch push-simple && + git config branch.push-simple.pushRemote from && + actual="$(${git_for_each_ref} \ + --format="%(push:remotename),%(push:remoteref)" \ + refs/heads/push-simple)" && + test from, = "$actual" + ) +' + +test_expect_success "${git_for_each_ref} --ignore-case ignores case" ' + ${git_for_each_ref} --format="%(refname)" refs/heads/MAIN >actual && + test_must_be_empty actual && + + echo refs/heads/main >expect && + ${git_for_each_ref} --format="%(refname)" --ignore-case \ + refs/heads/MAIN >actual && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} --omit-empty works" ' + ${git_for_each_ref} --format="%(refname)" >actual && + test_line_count -gt 1 actual && + ${git_for_each_ref} --format="%(if:equals=refs/heads/main)%(refname)%(then)%(refname)%(end)" --omit-empty >actual && + echo refs/heads/main >expect && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} --ignore-case works on multiple sort keys" ' + # name refs numerically to avoid case-insensitive filesystem conflicts + nr=0 && + for email in a A b B + do + for subject in a A b B + do + GIT_COMMITTER_EMAIL="$email@example.com" \ + git tag -m "tag $subject" icase-$(printf %02d $nr) && + nr=$((nr+1))|| + return 1 + done + done && + ${git_for_each_ref} --ignore-case \ + --format="%(taggeremail) %(subject) %(refname)" \ + --sort=refname \ + --sort=subject \ + --sort=taggeremail \ + refs/tags/icase-* >actual && + cat >expect <<-\EOF && + tag a refs/tags/icase-00 + tag A refs/tags/icase-01 + tag a refs/tags/icase-04 + tag A refs/tags/icase-05 + tag b refs/tags/icase-02 + tag B refs/tags/icase-03 + tag b refs/tags/icase-06 + tag B refs/tags/icase-07 + tag a refs/tags/icase-08 + tag A refs/tags/icase-09 + tag a refs/tags/icase-12 + tag A refs/tags/icase-13 + tag b refs/tags/icase-10 + tag B refs/tags/icase-11 + tag b refs/tags/icase-14 + tag B refs/tags/icase-15 + EOF + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} reports broken tags" ' + git tag -m "good tag" broken-tag-good HEAD && + git cat-file tag broken-tag-good >good && + sed s/commit/blob/ bad && + bad=$(git hash-object -w -t tag bad) && + git update-ref refs/tags/broken-tag-bad $bad && + test_must_fail ${git_for_each_ref} --format="%(*objectname)" \ + refs/tags/broken-tag-* +' + +test_expect_success 'set up tag with signature and no blank lines' ' + git tag -F - fake-sig-no-blanks <<-\EOF + this is the subject + -----BEGIN PGP SIGNATURE----- + not a real signature, but we just care about the + subject/body parsing. It is important here that + there are no blank lines in the signature. + -----END PGP SIGNATURE----- + EOF +' + +test_atom refs/tags/fake-sig-no-blanks contents:subject 'this is the subject' +test_atom refs/tags/fake-sig-no-blanks contents:body '' +test_atom refs/tags/fake-sig-no-blanks contents:signature "$sig" + +test_expect_success 'set up tag with CRLF signature' ' + append_cr <<-\EOF | + this is the subject + -----BEGIN PGP SIGNATURE----- + + not a real signature, but we just care about + the subject/body parsing. It is important here + that there is a blank line separating this + from the signature header. + -----END PGP SIGNATURE----- + EOF + git tag -F - --cleanup=verbatim fake-sig-crlf +' + +test_atom refs/tags/fake-sig-crlf contents:subject 'this is the subject' +test_atom refs/tags/fake-sig-crlf contents:body '' + +# CRLF is retained in the signature, so we have to pass our expected value +# through append_cr. But test_atom requires a shell string, which means command +# substitution, and the shell will strip trailing newlines from the output of +# the substitution. Hack around it by adding and then removing a dummy line. +sig_crlf="$(printf "%s" "$sig" | append_cr; echo dummy)" +sig_crlf=${sig_crlf%dummy} +test_atom refs/tags/fake-sig-crlf contents:signature "$sig_crlf" + +test_expect_success 'set up tag with signature and trailers' ' + git tag -F - fake-sig-trailer <<-\EOF + this is the subject + + this is the body + + My-Trailer: foo + -----BEGIN PGP SIGNATURE----- + + not a real signature, but we just care about the + subject/body/trailer parsing. + -----END PGP SIGNATURE----- + EOF +' + +# use "separator=" here to suppress the terminating newline +test_atom refs/tags/fake-sig-trailer trailers:separator= 'My-Trailer: foo' + +test_expect_success "${git_for_each_ref} --stdin: empty" ' + >in && + ${git_for_each_ref} --format="%(refname)" --stdin actual && + ${git_for_each_ref} --format="%(refname)" >expect && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} --stdin: fails if extra args" ' + >in && + test_must_fail ${git_for_each_ref} --format="%(refname)" \ + --stdin refs/heads/extra err && + grep "unknown arguments supplied with --stdin" err +' + +test_expect_success "${git_for_each_ref} --stdin: matches" ' + cat >in <<-EOF && + refs/tags/multi* + refs/heads/amb* + EOF + + cat >expect <<-EOF && + refs/heads/ambiguous + refs/tags/multi-ref1-100000-user1 + refs/tags/multi-ref1-100000-user2 + refs/tags/multi-ref1-200000-user1 + refs/tags/multi-ref1-200000-user2 + refs/tags/multi-ref2-100000-user1 + refs/tags/multi-ref2-100000-user2 + refs/tags/multi-ref2-200000-user1 + refs/tags/multi-ref2-200000-user2 + refs/tags/multiline + EOF + + ${git_for_each_ref} --format="%(refname)" --stdin actual && + test_cmp expect actual +' + +test_expect_success "${git_for_each_ref} with non-existing refs" ' + cat >in <<-EOF && + refs/heads/this-ref-does-not-exist + refs/tags/bogus + EOF + + ${git_for_each_ref} --format="%(refname)" --stdin actual && + test_must_be_empty actual && + + xargs ${git_for_each_ref} --format="%(refname)" actual && + test_must_be_empty actual +' + +test_expect_success "${git_for_each_ref} with nested tags" ' + git tag -am "Normal tag" nested/base HEAD && + git tag -am "Nested tag" nested/nest1 refs/tags/nested/base && + git tag -am "Double nested tag" nested/nest2 refs/tags/nested/nest1 && + + head_oid="$(git rev-parse HEAD)" && + base_tag_oid="$(git rev-parse refs/tags/nested/base)" && + nest1_tag_oid="$(git rev-parse refs/tags/nested/nest1)" && + nest2_tag_oid="$(git rev-parse refs/tags/nested/nest2)" && + + cat >expect <<-EOF && + refs/tags/nested/base $base_tag_oid tag $head_oid commit + refs/tags/nested/nest1 $nest1_tag_oid tag $head_oid commit + refs/tags/nested/nest2 $nest2_tag_oid tag $head_oid commit + EOF + + ${git_for_each_ref} \ + --format="%(refname) %(objectname) %(objecttype) %(*objectname) %(*objecttype)" \ + refs/tags/nested/ >actual && + test_cmp expect actual +' + +test_expect_success 'is-base atom with non-commits' ' + ${git_for_each_ref} --format="%(is-base:HEAD) %(refname)" >out 2>err && + grep "(HEAD) refs/heads/main" out && + + test_line_count = 2 err && + grep "error: object .* is a commit, not a blob" err && + grep "error: bad tag pointer to" err +' + +GRADE_FORMAT="%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" +TRUSTLEVEL_FORMAT="%(signature:trustlevel)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" + +test_expect_success GPG 'setup for signature atom using gpg' ' + git checkout -b signed && + + test_when_finished "test_unconfig commit.gpgSign" && + + echo "1" >file && + git add file && + test_tick && + git commit -S -m "file: 1" && + git tag first-signed && + + echo "2" >file && + test_tick && + git commit -a -m "file: 2" && + git tag second-unsigned && + + git config commit.gpgSign 1 && + echo "3" >file && + test_tick && + git commit -a --no-gpg-sign -m "file: 3" && + git tag third-unsigned && + + test_tick && + git rebase -f HEAD^^ && git tag second-signed HEAD^ && + git tag third-signed && + + echo "4" >file && + test_tick && + git commit -a -SB7227189 -m "file: 4" && + git tag fourth-signed && + + echo "5" >file && + test_tick && + git commit -a --no-gpg-sign -m "file: 5" && + git tag fifth-unsigned && + + echo "6" >file && + test_tick && + git commit -a --no-gpg-sign -m "file: 6" && + + test_tick && + git rebase -f HEAD^^ && + git tag fifth-signed HEAD^ && + git tag sixth-signed && + + echo "7" >file && + test_tick && + git commit -a --no-gpg-sign -m "file: 7" && + git tag seventh-unsigned +' + +test_expect_success GPGSSH 'setup for signature atom using ssh' ' + test_when_finished "test_unconfig gpg.format user.signingkey" && + + test_config gpg.format ssh && + test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" && + echo "8" >file && + test_tick && + git add file && + git commit -S -m "file: 8" && + git tag eighth-signed-ssh +' + +test_expect_success GPG2 'bare signature atom' ' + git verify-commit first-signed 2>expect && + echo >>expect && + ${git_for_each_ref} refs/tags/first-signed \ + --format="%(signature)" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show good signature with custom format' ' + git verify-commit first-signed && + cat >expect <<-\EOF && + G + 13B6F51ECDDE430D + C O Mitter + 73D758744BE721698EC54E8713B6F51ECDDE430D + 73D758744BE721698EC54E8713B6F51ECDDE430D + EOF + ${git_for_each_ref} refs/tags/first-signed \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' +test_expect_success GPGSSH 'show good signature with custom format with ssh' ' + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && + FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") && + cat >expect.tmpl <<-\EOF && + G + FINGERPRINT + principal with number 1 + FINGERPRINT + + EOF + sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect && + ${git_for_each_ref} refs/tags/eighth-signed-ssh \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'signature atom with grade option and bad signature' ' + git cat-file commit third-signed >raw && + sed -e "s/^file: 3/file: 3 forged/" raw >forged1 && + FORGED1=$(git hash-object -w -t commit forged1) && + git update-ref refs/tags/third-signed "$FORGED1" && + test_must_fail git verify-commit "$FORGED1" && + + cat >expect <<-\EOF && + B + 13B6F51ECDDE430D + C O Mitter + + + EOF + ${git_for_each_ref} refs/tags/third-signed \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show untrusted signature with custom format' ' + cat >expect <<-\EOF && + U + 65A0EEA02E30CAD7 + Eris Discordia + F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7 + D4BE22311AD3131E5EDA29A461092E85B7227189 + EOF + ${git_for_each_ref} refs/tags/fourth-signed \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show untrusted signature with undefined trust level' ' + cat >expect <<-\EOF && + undefined + 65A0EEA02E30CAD7 + Eris Discordia + F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7 + D4BE22311AD3131E5EDA29A461092E85B7227189 + EOF + ${git_for_each_ref} refs/tags/fourth-signed \ + --format="$TRUSTLEVEL_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show untrusted signature with ultimate trust level' ' + cat >expect <<-\EOF && + ultimate + 13B6F51ECDDE430D + C O Mitter + 73D758744BE721698EC54E8713B6F51ECDDE430D + 73D758744BE721698EC54E8713B6F51ECDDE430D + EOF + ${git_for_each_ref} refs/tags/sixth-signed \ + --format="$TRUSTLEVEL_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show unknown signature with custom format' ' + cat >expect <<-\EOF && + E + 13B6F51ECDDE430D + + + + EOF + GNUPGHOME="$GNUPGHOME_NOT_USED" ${git_for_each_ref} \ + refs/tags/sixth-signed --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_expect_success GPG 'show lack of signature with custom format' ' + cat >expect <<-\EOF && + N + + + + + EOF + ${git_for_each_ref} refs/tags/seventh-unsigned \ + --format="$GRADE_FORMAT" >actual && + test_cmp expect actual +' + +test_done diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh index ce9af79ab12723..1d9809114d2c8f 100755 --- a/t/t6300-for-each-ref.sh +++ b/t/t6300-for-each-ref.sh @@ -6,2150 +6,14 @@ test_description='for-each-ref test' . ./test-lib.sh -GNUPGHOME_NOT_USED=$GNUPGHOME -. "$TEST_DIRECTORY"/lib-gpg.sh -. "$TEST_DIRECTORY"/lib-terminal.sh -# Mon Jul 3 23:18:43 2006 +0000 -datestamp=1151968723 -setdate_and_increment () { - GIT_COMMITTER_DATE="$datestamp +0200" - datestamp=$(expr "$datestamp" + 1) - GIT_AUTHOR_DATE="$datestamp +0200" - datestamp=$(expr "$datestamp" + 1) - export GIT_COMMITTER_DATE GIT_AUTHOR_DATE -} - -test_object_file_size () { - oid=$(git rev-parse "$1") - path=".git/objects/$(test_oid_to_path $oid)" - test_file_size "$path" -} - -test_expect_success setup ' - # setup .mailmap - cat >.mailmap <<-EOF && - A Thor A U Thor - C Mitter C O Mitter - EOF - - setdate_and_increment && - echo "Using $datestamp" > one && - git add one && - git commit -m "Initial" && - git branch -M main && - setdate_and_increment && - git tag -a -m "Tagging at $datestamp" testtag && - git update-ref refs/remotes/origin/main main && - git remote add origin nowhere && - git config branch.main.remote origin && - git config branch.main.merge refs/heads/main && - git remote add myfork elsewhere && - git config remote.pushdefault myfork && - git config push.default current -' - -test_atom () { - case "$1" in - head) ref=refs/heads/main ;; - tag) ref=refs/tags/testtag ;; - sym) ref=refs/heads/sym ;; - *) ref=$1 ;; - esac - format=$2 - test_do=test_expect_${4:-success} - - printf '%s\n' "$3" >expected - $test_do $PREREQ "basic atom: $ref $format" ' - git for-each-ref --format="%($format)" "$ref" >actual && - sanitize_pgp actual.clean && - test_cmp expected actual.clean - ' - - # Automatically test "contents:size" atom after testing "contents" - if test "$format" = "contents" - then - # for commit leg, $3 is changed there - expect=$(printf '%s' "$3" | wc -c) - $test_do $PREREQ "basic atom: $ref contents:size" ' - type=$(git cat-file -t "$ref") && - case $type in - tag) - # We cannot use $3 as it expects sanitize_pgp to run - git cat-file tag $ref >out && - expect=$(tail -n +6 out | wc -c) && - rm -f out ;; - tree | blob) - expect="" ;; - commit) - : "use the calculated expect" ;; - *) - BUG "unknown object type" ;; - esac && - # Leave $expect unquoted to lose possible leading whitespaces - echo $expect >expected && - git for-each-ref --format="%(contents:size)" "$ref" >actual && - test_cmp expected actual - ' - fi -} - -hexlen=$(test_oid hexsz) - -test_atom head refname refs/heads/main -test_atom head refname: refs/heads/main -test_atom head refname:short main -test_atom head refname:lstrip=1 heads/main -test_atom head refname:lstrip=2 main -test_atom head refname:lstrip=-1 main -test_atom head refname:lstrip=-2 heads/main -test_atom head refname:rstrip=1 refs/heads -test_atom head refname:rstrip=2 refs -test_atom head refname:rstrip=-1 refs -test_atom head refname:rstrip=-2 refs/heads -test_atom head refname:strip=1 heads/main -test_atom head refname:strip=2 main -test_atom head refname:strip=-1 main -test_atom head refname:strip=-2 heads/main -test_atom head upstream refs/remotes/origin/main -test_atom head upstream:short origin/main -test_atom head upstream:lstrip=2 origin/main -test_atom head upstream:lstrip=-2 origin/main -test_atom head upstream:rstrip=2 refs/remotes -test_atom head upstream:rstrip=-2 refs/remotes -test_atom head upstream:strip=2 origin/main -test_atom head upstream:strip=-2 origin/main -test_atom head push refs/remotes/myfork/main -test_atom head push:short myfork/main -test_atom head push:lstrip=1 remotes/myfork/main -test_atom head push:lstrip=-1 main -test_atom head push:rstrip=1 refs/remotes/myfork -test_atom head push:rstrip=-1 refs -test_atom head push:strip=1 remotes/myfork/main -test_atom head push:strip=-1 main -test_atom head objecttype commit -test_atom head objectsize $((131 + hexlen)) -test_atom head objectsize:disk $(test_object_file_size refs/heads/main) -test_atom head deltabase $ZERO_OID -test_atom head objectname $(git rev-parse refs/heads/main) -test_atom head objectname:short $(git rev-parse --short refs/heads/main) -test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/main) -test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/main) -test_atom head tree $(git rev-parse refs/heads/main^{tree}) -test_atom head tree:short $(git rev-parse --short refs/heads/main^{tree}) -test_atom head tree:short=1 $(git rev-parse --short=1 refs/heads/main^{tree}) -test_atom head tree:short=10 $(git rev-parse --short=10 refs/heads/main^{tree}) -test_atom head parent '' -test_atom head parent:short '' -test_atom head parent:short=1 '' -test_atom head parent:short=10 '' -test_atom head numparent 0 -test_atom head object '' -test_atom head type '' -test_atom head raw "$(git cat-file commit refs/heads/main) -" -test_atom head '*objectname' '' -test_atom head '*objecttype' '' -test_atom head author 'A U Thor 1151968724 +0200' -test_atom head authorname 'A U Thor' -test_atom head authorname:mailmap 'A Thor' -test_atom head authoremail '' -test_atom head authoremail:trim 'author@example.com' -test_atom head authoremail:localpart 'author' -test_atom head authoremail:trim,localpart 'author' -test_atom head authoremail:mailmap '' -test_atom head authoremail:mailmap,trim 'athor@example.com' -test_atom head authoremail:trim,mailmap 'athor@example.com' -test_atom head authoremail:mailmap,localpart 'athor' -test_atom head authoremail:localpart,mailmap 'athor' -test_atom head authoremail:mailmap,trim,localpart,mailmap,trim 'athor' -test_atom head authordate 'Tue Jul 4 01:18:44 2006 +0200' -test_atom head committer 'C O Mitter 1151968723 +0200' -test_atom head committername 'C O Mitter' -test_atom head committername:mailmap 'C Mitter' -test_atom head committeremail '' -test_atom head committeremail:trim 'committer@example.com' -test_atom head committeremail:localpart 'committer' -test_atom head committeremail:localpart,trim 'committer' -test_atom head committeremail:mailmap '' -test_atom head committeremail:mailmap,trim 'cmitter@example.com' -test_atom head committeremail:trim,mailmap 'cmitter@example.com' -test_atom head committeremail:mailmap,localpart 'cmitter' -test_atom head committeremail:localpart,mailmap 'cmitter' -test_atom head committeremail:trim,mailmap,trim,trim,localpart 'cmitter' -test_atom head committerdate 'Tue Jul 4 01:18:43 2006 +0200' -test_atom head tag '' -test_atom head tagger '' -test_atom head taggername '' -test_atom head taggeremail '' -test_atom head taggeremail:trim '' -test_atom head taggeremail:localpart '' -test_atom head taggerdate '' -test_atom head creator 'C O Mitter 1151968723 +0200' -test_atom head creatordate 'Tue Jul 4 01:18:43 2006 +0200' -test_atom head subject 'Initial' -test_atom head subject:sanitize 'Initial' -test_atom head contents:subject 'Initial' -test_atom head body '' -test_atom head contents:body '' -test_atom head contents:signature '' -test_atom head contents 'Initial -' -test_atom head HEAD '*' - -test_atom tag refname refs/tags/testtag -test_atom tag refname:short testtag -test_atom tag upstream '' -test_atom tag push '' -test_atom tag objecttype tag -test_atom tag objectsize $((114 + hexlen)) -test_atom tag objectsize:disk $(test_object_file_size refs/tags/testtag) -test_atom tag '*objectsize:disk' $(test_object_file_size refs/heads/main) -test_atom tag deltabase $ZERO_OID -test_atom tag '*deltabase' $ZERO_OID -test_atom tag objectname $(git rev-parse refs/tags/testtag) -test_atom tag objectname:short $(git rev-parse --short refs/tags/testtag) -test_atom head objectname:short=1 $(git rev-parse --short=1 refs/heads/main) -test_atom head objectname:short=10 $(git rev-parse --short=10 refs/heads/main) -test_atom tag tree '' -test_atom tag tree:short '' -test_atom tag tree:short=1 '' -test_atom tag tree:short=10 '' -test_atom tag parent '' -test_atom tag parent:short '' -test_atom tag parent:short=1 '' -test_atom tag parent:short=10 '' -test_atom tag numparent '' -test_atom tag object $(git rev-parse refs/tags/testtag^0) -test_atom tag type 'commit' -test_atom tag '*objectname' $(git rev-parse refs/tags/testtag^{}) -test_atom tag '*objecttype' 'commit' -test_atom tag author '' -test_atom tag authorname '' -test_atom tag authorname:mailmap '' -test_atom tag authoremail '' -test_atom tag authoremail:trim '' -test_atom tag authoremail:localpart '' -test_atom tag authoremail:trim,localpart '' -test_atom tag authoremail:mailmap '' -test_atom tag authoremail:mailmap,trim '' -test_atom tag authoremail:trim,mailmap '' -test_atom tag authoremail:mailmap,localpart '' -test_atom tag authoremail:localpart,mailmap '' -test_atom tag authoremail:mailmap,trim,localpart,mailmap,trim '' -test_atom tag authordate '' -test_atom tag committer '' -test_atom tag committername '' -test_atom tag committername:mailmap '' -test_atom tag committeremail '' -test_atom tag committeremail:trim '' -test_atom tag committeremail:localpart '' -test_atom tag committeremail:localpart,trim '' -test_atom tag committeremail:mailmap '' -test_atom tag committeremail:mailmap,trim '' -test_atom tag committeremail:trim,mailmap '' -test_atom tag committeremail:mailmap,localpart '' -test_atom tag committeremail:localpart,mailmap '' -test_atom tag committeremail:trim,mailmap,trim,trim,localpart '' -test_atom tag committerdate '' -test_atom tag tag 'testtag' -test_atom tag tagger 'C O Mitter 1151968725 +0200' -test_atom tag taggername 'C O Mitter' -test_atom tag taggername:mailmap 'C Mitter' -test_atom tag taggeremail '' -test_atom tag taggeremail:trim 'committer@example.com' -test_atom tag taggeremail:localpart 'committer' -test_atom tag taggeremail:trim,localpart 'committer' -test_atom tag taggeremail:mailmap '' -test_atom tag taggeremail:mailmap,trim 'cmitter@example.com' -test_atom tag taggeremail:trim,mailmap 'cmitter@example.com' -test_atom tag taggeremail:mailmap,localpart 'cmitter' -test_atom tag taggeremail:localpart,mailmap 'cmitter' -test_atom tag taggeremail:trim,mailmap,trim,localpart,localpart 'cmitter' -test_atom tag taggerdate 'Tue Jul 4 01:18:45 2006 +0200' -test_atom tag creator 'C O Mitter 1151968725 +0200' -test_atom tag creatordate 'Tue Jul 4 01:18:45 2006 +0200' -test_atom tag subject 'Tagging at 1151968727' -test_atom tag subject:sanitize 'Tagging-at-1151968727' -test_atom tag contents:subject 'Tagging at 1151968727' -test_atom tag body '' -test_atom tag contents:body '' -test_atom tag contents:signature '' -test_atom tag contents 'Tagging at 1151968727 -' -test_atom tag HEAD ' ' - -test_expect_success 'basic atom: refs/tags/testtag *raw' ' - git cat-file commit refs/tags/testtag^{} >expected && - git for-each-ref --format="%(*raw)" refs/tags/testtag >actual && - sanitize_pgp expected.clean && - echo >>expected.clean && - sanitize_pgp actual.clean && - test_cmp expected.clean actual.clean -' - -test_expect_success 'Check invalid atoms names are errors' ' - test_must_fail git for-each-ref --format="%(INVALID)" refs/heads -' - -test_expect_success 'for-each-ref does not crash with -h' ' +test_expect_success "for-each-ref does not crash with -h" ' test_expect_code 129 git for-each-ref -h >usage && test_grep "[Uu]sage: git for-each-ref " usage && test_expect_code 129 nongit git for-each-ref -h >usage && test_grep "[Uu]sage: git for-each-ref " usage ' -test_expect_success 'Check format specifiers are ignored in naming date atoms' ' - git for-each-ref --format="%(authordate)" refs/heads && - git for-each-ref --format="%(authordate:default) %(authordate)" refs/heads && - git for-each-ref --format="%(authordate) %(authordate:default)" refs/heads && - git for-each-ref --format="%(authordate:default) %(authordate:default)" refs/heads -' - -test_expect_success 'Check valid format specifiers for date fields' ' - git for-each-ref --format="%(authordate:default)" refs/heads && - git for-each-ref --format="%(authordate:relative)" refs/heads && - git for-each-ref --format="%(authordate:short)" refs/heads && - git for-each-ref --format="%(authordate:local)" refs/heads && - git for-each-ref --format="%(authordate:iso8601)" refs/heads && - git for-each-ref --format="%(authordate:rfc2822)" refs/heads -' - -test_expect_success 'Check invalid format specifiers are errors' ' - test_must_fail git for-each-ref --format="%(authordate:INVALID)" refs/heads -' - -test_expect_success 'arguments to %(objectname:short=) must be positive integers' ' - test_must_fail git for-each-ref --format="%(objectname:short=0)" && - test_must_fail git for-each-ref --format="%(objectname:short=-1)" && - test_must_fail git for-each-ref --format="%(objectname:short=foo)" -' - -test_bad_atom () { - case "$1" in - head) ref=refs/heads/main ;; - tag) ref=refs/tags/testtag ;; - sym) ref=refs/heads/sym ;; - *) ref=$1 ;; - esac - format=$2 - test_do=test_expect_${4:-success} - - printf '%s\n' "$3" >expect - $test_do $PREREQ "err basic atom: $ref $format" ' - test_must_fail git for-each-ref \ - --format="%($format)" "$ref" 2>error && - test_cmp expect error - ' -} - -test_bad_atom head 'authoremail:foo' \ - 'fatal: unrecognized %(authoremail) argument: foo' - -test_bad_atom head 'authoremail:mailmap,trim,bar' \ - 'fatal: unrecognized %(authoremail) argument: bar' - -test_bad_atom head 'authoremail:trim,' \ - 'fatal: unrecognized %(authoremail) argument: ' - -test_bad_atom head 'authoremail:mailmaptrim' \ - 'fatal: unrecognized %(authoremail) argument: trim' - -test_bad_atom head 'committeremail: ' \ - 'fatal: unrecognized %(committeremail) argument: ' - -test_bad_atom head 'committeremail: trim,foo' \ - 'fatal: unrecognized %(committeremail) argument: trim,foo' - -test_bad_atom head 'committeremail:mailmap,localpart ' \ - 'fatal: unrecognized %(committeremail) argument: ' - -test_bad_atom head 'committeremail:trim_localpart' \ - 'fatal: unrecognized %(committeremail) argument: _localpart' - -test_bad_atom head 'committeremail:localpart,,,trim' \ - 'fatal: unrecognized %(committeremail) argument: ,,trim' - -test_bad_atom tag 'taggeremail:mailmap,trim, foo ' \ - 'fatal: unrecognized %(taggeremail) argument: foo ' - -test_bad_atom tag 'taggeremail:trim,localpart,' \ - 'fatal: unrecognized %(taggeremail) argument: ' - -test_bad_atom tag 'taggeremail:mailmap;localpart trim' \ - 'fatal: unrecognized %(taggeremail) argument: ;localpart trim' - -test_bad_atom tag 'taggeremail:localpart trim' \ - 'fatal: unrecognized %(taggeremail) argument: trim' - -test_bad_atom tag 'taggeremail:mailmap,mailmap,trim,qux,localpart,trim' \ - 'fatal: unrecognized %(taggeremail) argument: qux,localpart,trim' - -test_date () { - f=$1 && - committer_date=$2 && - author_date=$3 && - tagger_date=$4 && - cat >expected <<-EOF && - 'refs/heads/main' '$committer_date' '$author_date' - 'refs/tags/testtag' '$tagger_date' - EOF - ( - git for-each-ref --shell \ - --format="%(refname) %(committerdate${f:+:$f}) %(authordate${f:+:$f})" \ - refs/heads && - git for-each-ref --shell \ - --format="%(refname) %(taggerdate${f:+:$f})" \ - refs/tags - ) >actual && - test_cmp expected actual -} - -test_expect_success 'Check unformatted date fields output' ' - test_date "" \ - "Tue Jul 4 01:18:43 2006 +0200" \ - "Tue Jul 4 01:18:44 2006 +0200" \ - "Tue Jul 4 01:18:45 2006 +0200" -' - -test_expect_success 'Check format "default" formatted date fields output' ' - test_date default \ - "Tue Jul 4 01:18:43 2006 +0200" \ - "Tue Jul 4 01:18:44 2006 +0200" \ - "Tue Jul 4 01:18:45 2006 +0200" -' - -test_expect_success 'Check format "default-local" date fields output' ' - test_date default-local "Mon Jul 3 23:18:43 2006" "Mon Jul 3 23:18:44 2006" "Mon Jul 3 23:18:45 2006" -' - -# Don't know how to do relative check because I can't know when this script -# is going to be run and can't fake the current time to git, and hence can't -# provide expected output. Instead, I'll just make sure that "relative" -# doesn't exit in error -test_expect_success 'Check format "relative" date fields output' ' - f=relative && - (git for-each-ref --shell --format="%(refname) %(committerdate:$f) %(authordate:$f)" refs/heads && - git for-each-ref --shell --format="%(refname) %(taggerdate:$f)" refs/tags) >actual -' - -# We just check that this is the same as "relative" for now. -test_expect_success 'Check format "relative-local" date fields output' ' - test_date relative-local \ - "$(git for-each-ref --format="%(committerdate:relative)" refs/heads)" \ - "$(git for-each-ref --format="%(authordate:relative)" refs/heads)" \ - "$(git for-each-ref --format="%(taggerdate:relative)" refs/tags)" -' - -test_expect_success 'Check format "short" date fields output' ' - test_date short 2006-07-04 2006-07-04 2006-07-04 -' - -test_expect_success 'Check format "short-local" date fields output' ' - test_date short-local 2006-07-03 2006-07-03 2006-07-03 -' - -test_expect_success 'Check format "local" date fields output' ' - test_date local \ - "Mon Jul 3 23:18:43 2006" \ - "Mon Jul 3 23:18:44 2006" \ - "Mon Jul 3 23:18:45 2006" -' - -test_expect_success 'Check format "iso8601" date fields output' ' - test_date iso8601 \ - "2006-07-04 01:18:43 +0200" \ - "2006-07-04 01:18:44 +0200" \ - "2006-07-04 01:18:45 +0200" -' - -test_expect_success 'Check format "iso8601-local" date fields output' ' - test_date iso8601-local "2006-07-03 23:18:43 +0000" "2006-07-03 23:18:44 +0000" "2006-07-03 23:18:45 +0000" -' - -test_expect_success 'Check format "rfc2822" date fields output' ' - test_date rfc2822 \ - "Tue, 4 Jul 2006 01:18:43 +0200" \ - "Tue, 4 Jul 2006 01:18:44 +0200" \ - "Tue, 4 Jul 2006 01:18:45 +0200" -' - -test_expect_success 'Check format "rfc2822-local" date fields output' ' - test_date rfc2822-local "Mon, 3 Jul 2006 23:18:43 +0000" "Mon, 3 Jul 2006 23:18:44 +0000" "Mon, 3 Jul 2006 23:18:45 +0000" -' - -test_expect_success 'Check format "raw" date fields output' ' - test_date raw "1151968723 +0200" "1151968724 +0200" "1151968725 +0200" -' - -test_expect_success 'Check format "raw-local" date fields output' ' - test_date raw-local "1151968723 +0000" "1151968724 +0000" "1151968725 +0000" -' - -test_expect_success 'Check format of strftime date fields' ' - echo "my date is 2006-07-04" >expected && - git for-each-ref \ - --format="%(authordate:format:my date is %Y-%m-%d)" \ - refs/heads >actual && - test_cmp expected actual -' - -test_expect_success 'Check format of strftime-local date fields' ' - echo "my date is 2006-07-03" >expected && - git for-each-ref \ - --format="%(authordate:format-local:my date is %Y-%m-%d)" \ - refs/heads >actual && - test_cmp expected actual -' - -test_expect_success 'exercise strftime with odd fields' ' - echo >expected && - git for-each-ref --format="%(authordate:format:)" refs/heads >actual && - test_cmp expected actual && - long="long format -- $ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID$ZERO_OID" && - echo $long >expected && - git for-each-ref --format="%(authordate:format:$long)" refs/heads >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -refs/heads/main -refs/remotes/origin/main -refs/tags/testtag -EOF - -test_expect_success 'Verify ascending sort' ' - git for-each-ref --format="%(refname)" --sort=refname >actual && - test_cmp expected actual -' - - -cat >expected <<\EOF -refs/tags/testtag -refs/remotes/origin/main -refs/heads/main -EOF - -test_expect_success 'Verify descending sort' ' - git for-each-ref --format="%(refname)" --sort=-refname >actual && - test_cmp expected actual -' - -test_expect_success 'Give help even with invalid sort atoms' ' - test_expect_code 129 git for-each-ref --sort=bogus -h >actual 2>&1 && - grep "^usage: git for-each-ref" actual -' - -cat >expected <<\EOF -refs/tags/testtag -refs/tags/testtag-2 -EOF - -test_expect_success 'exercise patterns with prefixes' ' - git tag testtag-2 && - test_when_finished "git tag -d testtag-2" && - git for-each-ref --format="%(refname)" \ - refs/tags/testtag refs/tags/testtag-2 >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -refs/tags/testtag -refs/tags/testtag-2 -EOF - -test_expect_success 'exercise glob patterns with prefixes' ' - git tag testtag-2 && - test_when_finished "git tag -d testtag-2" && - git for-each-ref --format="%(refname)" \ - refs/tags/testtag "refs/tags/testtag-*" >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -refs/tags/bar -refs/tags/baz -refs/tags/testtag -EOF - -test_expect_success 'exercise patterns with prefix exclusions' ' - for tag in foo/one foo/two foo/three bar baz - do - git tag "$tag" || return 1 - done && - test_when_finished "git tag -d foo/one foo/two foo/three bar baz" && - git for-each-ref --format="%(refname)" \ - refs/tags/ --exclude=refs/tags/foo >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -refs/tags/bar -refs/tags/baz -refs/tags/foo/one -refs/tags/testtag -EOF - -test_expect_success 'exercise patterns with pattern exclusions' ' - for tag in foo/one foo/two foo/three bar baz - do - git tag "$tag" || return 1 - done && - test_when_finished "git tag -d foo/one foo/two foo/three bar baz" && - git for-each-ref --format="%(refname)" \ - refs/tags/ --exclude="refs/tags/foo/t*" >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -'refs/heads/main' -'refs/remotes/origin/main' -'refs/tags/testtag' -EOF - -test_expect_success 'Quoting style: shell' ' - git for-each-ref --shell --format="%(refname)" >actual && - test_cmp expected actual -' - -test_expect_success 'Quoting style: perl' ' - git for-each-ref --perl --format="%(refname)" >actual && - test_cmp expected actual -' - -test_expect_success 'Quoting style: python' ' - git for-each-ref --python --format="%(refname)" >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -"refs/heads/main" -"refs/remotes/origin/main" -"refs/tags/testtag" -EOF - -test_expect_success 'Quoting style: tcl' ' - git for-each-ref --tcl --format="%(refname)" >actual && - test_cmp expected actual -' - -for i in "--perl --shell" "-s --python" "--python --tcl" "--tcl --perl"; do - test_expect_success "more than one quoting style: $i" " - test_must_fail git for-each-ref $i 2>err && - grep '^error: more than one quoting style' err - " -done - -test_expect_success 'setup for upstream:track[short]' ' - test_commit two -' - -test_atom head upstream:track '[ahead 1]' -test_atom head upstream:trackshort '>' -test_atom head upstream:track,nobracket 'ahead 1' -test_atom head upstream:nobracket,track 'ahead 1' - -test_expect_success 'setup for push:track[short]' ' - test_commit third && - git update-ref refs/remotes/myfork/main main && - git reset main~1 -' - -test_atom head push:track '[behind 1]' -test_atom head push:trackshort '<' - -test_expect_success 'Check that :track[short] cannot be used with other atoms' ' - test_must_fail git for-each-ref --format="%(refname:track)" 2>/dev/null && - test_must_fail git for-each-ref --format="%(refname:trackshort)" 2>/dev/null -' - -test_expect_success 'Check that :track[short] works when upstream is invalid' ' - cat >expected <<-\EOF && - [gone] - - EOF - test_when_finished "git config branch.main.merge refs/heads/main" && - git config branch.main.merge refs/heads/does-not-exist && - git for-each-ref \ - --format="%(upstream:track)$LF%(upstream:trackshort)" \ - refs/heads >actual && - test_cmp expected actual -' - -test_expect_success 'Check for invalid refname format' ' - test_must_fail git for-each-ref --format="%(refname:INVALID)" -' - -test_expect_success 'set up color tests' ' - cat >expected.color <<-EOF && - $(git rev-parse --short refs/heads/main) main - $(git rev-parse --short refs/remotes/myfork/main) myfork/main - $(git rev-parse --short refs/remotes/origin/main) origin/main - $(git rev-parse --short refs/tags/testtag) testtag - $(git rev-parse --short refs/tags/third) third - $(git rev-parse --short refs/tags/two) two - EOF - sed "s/<[^>]*>//g" expected.bare && - color_format="%(objectname:short) %(color:green)%(refname:short)" -' - -test_expect_success TTY '%(color) shows color with a tty' ' - test_terminal git for-each-ref --format="$color_format" >actual.raw && - test_decode_color actual && - test_cmp expected.color actual -' - -test_expect_success '%(color) does not show color without tty' ' - TERM=vt100 git for-each-ref --format="$color_format" >actual && - test_cmp expected.bare actual -' - -test_expect_success '--color can override tty check' ' - git for-each-ref --color --format="$color_format" >actual.raw && - test_decode_color actual && - test_cmp expected.color actual -' - -test_expect_success 'color.ui=always does not override tty check' ' - git -c color.ui=always for-each-ref --format="$color_format" >actual && - test_cmp expected.bare actual -' - -test_expect_success 'setup for describe atom tests' ' - git init -b master describe-repo && - ( - cd describe-repo && - - test_commit --no-tag one && - git tag tagone && - - test_commit --no-tag two && - git tag -a -m "tag two" tagtwo - ) -' - -test_expect_success 'describe atom vs git describe' ' - ( - cd describe-repo && - - git for-each-ref --format="%(objectname)" \ - refs/tags/ >obj && - while read hash - do - if desc=$(git describe $hash) - then - : >expect-contains-good - else - : >expect-contains-bad - fi && - echo "$hash $desc" || return 1 - done expect && - test_path_exists expect-contains-good && - test_path_exists expect-contains-bad && - - git for-each-ref --format="%(objectname) %(describe)" \ - refs/tags/ >actual 2>err && - test_cmp expect actual && - test_must_be_empty err - ) -' - -test_expect_success 'describe:tags vs describe --tags' ' - ( - cd describe-repo && - git describe --tags >expect && - git for-each-ref --format="%(describe:tags)" \ - refs/heads/master >actual && - test_cmp expect actual - ) -' - -test_expect_success 'describe:abbrev=... vs describe --abbrev=...' ' - ( - cd describe-repo && - - # Case 1: We have commits between HEAD and the most - # recent tag reachable from it - test_commit --no-tag file && - git describe --abbrev=14 >expect && - git for-each-ref --format="%(describe:abbrev=14)" \ - refs/heads/master >actual && - test_cmp expect actual && - - # Make sure the hash used is at least 14 digits long - sed -e "s/^.*-g\([0-9a-f]*\)$/\1/" hexpart && - test 15 -le $(wc -c expect && - git for-each-ref --format="%(describe:abbrev=14)" \ - refs/heads/master >actual && - test_cmp expect actual && - test tagname = $(cat actual) - ) -' - -test_expect_success 'describe:match=... vs describe --match ...' ' - ( - cd describe-repo && - git tag -a -m "tag foo" tag-foo && - git describe --match "*-foo" >expect && - git for-each-ref --format="%(describe:match="*-foo")" \ - refs/heads/master >actual && - test_cmp expect actual - ) -' - -test_expect_success 'describe:exclude:... vs describe --exclude ...' ' - ( - cd describe-repo && - git tag -a -m "tag bar" tag-bar && - git describe --exclude "*-bar" >expect && - git for-each-ref --format="%(describe:exclude="*-bar")" \ - refs/heads/master >actual && - test_cmp expect actual - ) -' - -test_expect_success 'deref with describe atom' ' - ( - cd describe-repo && - cat >expect <<-\EOF && - - tagname - tagname - tagname - - tagtwo - EOF - git for-each-ref --format="%(*describe)" >actual && - test_cmp expect actual - ) -' - -test_expect_success 'err on bad describe atom arg' ' - ( - cd describe-repo && - - # The bad arg is the only arg passed to describe atom - cat >expect <<-\EOF && - fatal: unrecognized %(describe) argument: baz - EOF - test_must_fail git for-each-ref --format="%(describe:baz)" \ - refs/heads/master 2>actual && - test_cmp expect actual && - - # The bad arg is in the middle of the option string - # passed to the describe atom - cat >expect <<-\EOF && - fatal: unrecognized %(describe) argument: qux=1,abbrev=14 - EOF - test_must_fail git for-each-ref \ - --format="%(describe:tags,qux=1,abbrev=14)" \ - ref/heads/master 2>actual && - test_cmp expect actual - ) -' - -cat >expected <<\EOF -heads/main -tags/main -EOF - -test_expect_success 'Check ambiguous head and tag refs (strict)' ' - git config --bool core.warnambiguousrefs true && - git checkout -b newtag && - echo "Using $datestamp" > one && - git add one && - git commit -m "Branch" && - setdate_and_increment && - git tag -m "Tagging at $datestamp" main && - git for-each-ref --format "%(refname:short)" refs/heads/main refs/tags/main >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -heads/main -main -EOF - -test_expect_success 'Check ambiguous head and tag refs (loose)' ' - git config --bool core.warnambiguousrefs false && - git for-each-ref --format "%(refname:short)" refs/heads/main refs/tags/main >actual && - test_cmp expected actual -' - -cat >expected <<\EOF -heads/ambiguous -ambiguous -EOF - -test_expect_success 'Check ambiguous head and tag refs II (loose)' ' - git checkout main && - git tag ambiguous testtag^0 && - git branch ambiguous testtag^0 && - git for-each-ref --format "%(refname:short)" refs/heads/ambiguous refs/tags/ambiguous >actual && - test_cmp expected actual -' - -test_expect_success 'create tag without tagger' ' - git tag -a -m "Broken tag" taggerless && - git tag -f taggerless $(git cat-file tag taggerless | - sed -e "/^tagger /d" | - git hash-object --literally --stdin -w -t tag) -' - -test_atom refs/tags/taggerless type 'commit' -test_atom refs/tags/taggerless tag 'taggerless' -test_atom refs/tags/taggerless tagger '' -test_atom refs/tags/taggerless taggername '' -test_atom refs/tags/taggerless taggeremail '' -test_atom refs/tags/taggerless taggeremail:trim '' -test_atom refs/tags/taggerless taggeremail:localpart '' -test_atom refs/tags/taggerless taggerdate '' -test_atom refs/tags/taggerless committer '' -test_atom refs/tags/taggerless committername '' -test_atom refs/tags/taggerless committeremail '' -test_atom refs/tags/taggerless committeremail:trim '' -test_atom refs/tags/taggerless committeremail:localpart '' -test_atom refs/tags/taggerless committerdate '' -test_atom refs/tags/taggerless subject 'Broken tag' - -test_expect_success 'an unusual tag with an incomplete line' ' - - git tag -m "bogo" bogo && - bogo=$(git cat-file tag bogo) && - bogo=$(printf "%s" "$bogo" | git mktag) && - git tag -f bogo "$bogo" && - git for-each-ref --format "%(body)" refs/tags/bogo - -' - -test_expect_success 'create tag with subject and body content' ' - cat >>msg <<-\EOF && - the subject line - - first body line - second body line - EOF - git tag -F msg subject-body -' -test_atom refs/tags/subject-body subject 'the subject line' -test_atom refs/tags/subject-body subject:sanitize 'the-subject-line' -test_atom refs/tags/subject-body body 'first body line -second body line -' -test_atom refs/tags/subject-body contents 'the subject line - -first body line -second body line -' - -test_expect_success 'create tag with multiline subject' ' - cat >msg <<-\EOF && - first subject line - second subject line - - first body line - second body line - EOF - git tag -F msg multiline -' -test_atom refs/tags/multiline subject 'first subject line second subject line' -test_atom refs/tags/multiline subject:sanitize 'first-subject-line-second-subject-line' -test_atom refs/tags/multiline contents:subject 'first subject line second subject line' -test_atom refs/tags/multiline body 'first body line -second body line -' -test_atom refs/tags/multiline contents:body 'first body line -second body line -' -test_atom refs/tags/multiline contents:signature '' -test_atom refs/tags/multiline contents 'first subject line -second subject line - -first body line -second body line -' - -test_expect_success GPG 'create signed tags' ' - git tag -s -m "" signed-empty && - git tag -s -m "subject line" signed-short && - cat >msg <<-\EOF && - subject line - - body contents - EOF - git tag -s -F msg signed-long -' - -sig='-----BEGIN PGP SIGNATURE----- ------END PGP SIGNATURE----- -' - -PREREQ=GPG -test_atom refs/tags/signed-empty subject '' -test_atom refs/tags/signed-empty subject:sanitize '' -test_atom refs/tags/signed-empty contents:subject '' -test_atom refs/tags/signed-empty body "$sig" -test_atom refs/tags/signed-empty contents:body '' -test_atom refs/tags/signed-empty contents:signature "$sig" -test_atom refs/tags/signed-empty contents "$sig" - -test_expect_success GPG 'basic atom: refs/tags/signed-empty raw' ' - git cat-file tag refs/tags/signed-empty >expected && - git for-each-ref --format="%(raw)" refs/tags/signed-empty >actual && - sanitize_pgp expected.clean && - echo >>expected.clean && - sanitize_pgp actual.clean && - test_cmp expected.clean actual.clean -' - -test_atom refs/tags/signed-short subject 'subject line' -test_atom refs/tags/signed-short subject:sanitize 'subject-line' -test_atom refs/tags/signed-short contents:subject 'subject line' -test_atom refs/tags/signed-short body "$sig" -test_atom refs/tags/signed-short contents:body '' -test_atom refs/tags/signed-short contents:signature "$sig" -test_atom refs/tags/signed-short contents "subject line -$sig" - -test_expect_success GPG 'basic atom: refs/tags/signed-short raw' ' - git cat-file tag refs/tags/signed-short >expected && - git for-each-ref --format="%(raw)" refs/tags/signed-short >actual && - sanitize_pgp expected.clean && - echo >>expected.clean && - sanitize_pgp actual.clean && - test_cmp expected.clean actual.clean -' - -test_atom refs/tags/signed-long subject 'subject line' -test_atom refs/tags/signed-long subject:sanitize 'subject-line' -test_atom refs/tags/signed-long contents:subject 'subject line' -test_atom refs/tags/signed-long body "body contents -$sig" -test_atom refs/tags/signed-long contents:body 'body contents -' -test_atom refs/tags/signed-long contents:signature "$sig" -test_atom refs/tags/signed-long contents "subject line - -body contents -$sig" - -test_expect_success GPG 'basic atom: refs/tags/signed-long raw' ' - git cat-file tag refs/tags/signed-long >expected && - git for-each-ref --format="%(raw)" refs/tags/signed-long >actual && - sanitize_pgp expected.clean && - echo >>expected.clean && - sanitize_pgp actual.clean && - test_cmp expected.clean actual.clean -' - -test_expect_success 'set up refs pointing to tree and blob' ' - git update-ref refs/mytrees/first refs/heads/main^{tree} && - git update-ref refs/myblobs/first refs/heads/main:one -' - -test_atom refs/mytrees/first subject "" -test_atom refs/mytrees/first contents:subject "" -test_atom refs/mytrees/first body "" -test_atom refs/mytrees/first contents:body "" -test_atom refs/mytrees/first contents:signature "" -test_atom refs/mytrees/first contents "" - -test_expect_success 'basic atom: refs/mytrees/first raw' ' - git cat-file tree refs/mytrees/first >expected && - echo >>expected && - git for-each-ref --format="%(raw)" refs/mytrees/first >actual && - test_cmp expected actual && - git cat-file -s refs/mytrees/first >expected && - git for-each-ref --format="%(raw:size)" refs/mytrees/first >actual && - test_cmp expected actual -' - -test_atom refs/myblobs/first subject "" -test_atom refs/myblobs/first contents:subject "" -test_atom refs/myblobs/first body "" -test_atom refs/myblobs/first contents:body "" -test_atom refs/myblobs/first contents:signature "" -test_atom refs/myblobs/first contents "" - -test_expect_success 'basic atom: refs/myblobs/first raw' ' - git cat-file blob refs/myblobs/first >expected && - echo >>expected && - git for-each-ref --format="%(raw)" refs/myblobs/first >actual && - test_cmp expected actual && - git cat-file -s refs/myblobs/first >expected && - git for-each-ref --format="%(raw:size)" refs/myblobs/first >actual && - test_cmp expected actual -' - -test_expect_success 'set up refs pointing to binary blob' ' - printf "a\0b\0c" >blob1 && - printf "a\0c\0b" >blob2 && - printf "\0a\0b\0c" >blob3 && - printf "abc" >blob4 && - printf "\0 \0 \0 " >blob5 && - printf "\0 \0a\0 " >blob6 && - printf " " >blob7 && - >blob8 && - obj=$(git hash-object -w blob1) && - git update-ref refs/myblobs/blob1 "$obj" && - obj=$(git hash-object -w blob2) && - git update-ref refs/myblobs/blob2 "$obj" && - obj=$(git hash-object -w blob3) && - git update-ref refs/myblobs/blob3 "$obj" && - obj=$(git hash-object -w blob4) && - git update-ref refs/myblobs/blob4 "$obj" && - obj=$(git hash-object -w blob5) && - git update-ref refs/myblobs/blob5 "$obj" && - obj=$(git hash-object -w blob6) && - git update-ref refs/myblobs/blob6 "$obj" && - obj=$(git hash-object -w blob7) && - git update-ref refs/myblobs/blob7 "$obj" && - obj=$(git hash-object -w blob8) && - git update-ref refs/myblobs/blob8 "$obj" -' - -test_expect_success 'Verify sorts with raw' ' - cat >expected <<-EOF && - refs/myblobs/blob8 - refs/myblobs/blob5 - refs/myblobs/blob6 - refs/myblobs/blob3 - refs/myblobs/blob7 - refs/mytrees/first - refs/myblobs/first - refs/myblobs/blob1 - refs/myblobs/blob2 - refs/myblobs/blob4 - refs/heads/main - EOF - git for-each-ref --format="%(refname)" --sort=raw \ - refs/heads/main refs/myblobs/ refs/mytrees/first >actual && - test_cmp expected actual -' - -test_expect_success 'Verify sorts with raw:size' ' - cat >expected <<-EOF && - refs/myblobs/blob8 - refs/myblobs/blob7 - refs/myblobs/blob4 - refs/myblobs/blob1 - refs/myblobs/blob2 - refs/myblobs/blob3 - refs/myblobs/blob5 - refs/myblobs/blob6 - refs/myblobs/first - refs/mytrees/first - refs/heads/main - EOF - git for-each-ref --format="%(refname)" --sort=raw:size \ - refs/heads/main refs/myblobs/ refs/mytrees/first >actual && - test_cmp expected actual -' - -test_expect_success 'validate raw atom with %(if:equals)' ' - cat >expected <<-EOF && - not equals - not equals - not equals - not equals - not equals - not equals - refs/myblobs/blob4 - not equals - not equals - not equals - not equals - not equals - EOF - git for-each-ref --format="%(if:equals=abc)%(raw)%(then)%(refname)%(else)not equals%(end)" \ - refs/myblobs/ refs/heads/ >actual && - test_cmp expected actual -' - -test_expect_success 'validate raw atom with %(if:notequals)' ' - cat >expected <<-EOF && - refs/heads/ambiguous - refs/heads/main - refs/heads/newtag - refs/myblobs/blob1 - refs/myblobs/blob2 - refs/myblobs/blob3 - equals - refs/myblobs/blob5 - refs/myblobs/blob6 - refs/myblobs/blob7 - refs/myblobs/blob8 - refs/myblobs/first - EOF - git for-each-ref --format="%(if:notequals=abc)%(raw)%(then)%(refname)%(else)equals%(end)" \ - refs/myblobs/ refs/heads/ >actual && - test_cmp expected actual -' - -test_expect_success 'empty raw refs with %(if)' ' - cat >expected <<-EOF && - refs/myblobs/blob1 not empty - refs/myblobs/blob2 not empty - refs/myblobs/blob3 not empty - refs/myblobs/blob4 not empty - refs/myblobs/blob5 not empty - refs/myblobs/blob6 not empty - refs/myblobs/blob7 empty - refs/myblobs/blob8 empty - refs/myblobs/first not empty - EOF - git for-each-ref --format="%(refname) %(if)%(raw)%(then)not empty%(else)empty%(end)" \ - refs/myblobs/ >actual && - test_cmp expected actual -' - -test_expect_success '%(raw) with --python must fail' ' - test_must_fail git for-each-ref --format="%(raw)" --python -' - -test_expect_success '%(raw) with --tcl must fail' ' - test_must_fail git for-each-ref --format="%(raw)" --tcl -' - -test_expect_success PERL_TEST_HELPERS '%(raw) with --perl' ' - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/myblobs/blob1 --perl | perl >actual && - cmp blob1 actual && - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/myblobs/blob3 --perl | perl >actual && - cmp blob3 actual && - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/myblobs/blob8 --perl | perl >actual && - cmp blob8 actual && - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/myblobs/first --perl | perl >actual && - cmp one actual && - git cat-file tree refs/mytrees/first > expected && - git for-each-ref --format="\$name= %(raw); -print \"\$name\"" refs/mytrees/first --perl | perl >actual && - cmp expected actual -' - -test_expect_success '%(raw) with --shell must fail' ' - test_must_fail git for-each-ref --format="%(raw)" --shell -' - -test_expect_success '%(raw) with --shell and --sort=raw must fail' ' - test_must_fail git for-each-ref --format="%(raw)" --sort=raw --shell -' - -test_expect_success '%(raw:size) with --shell' ' - git for-each-ref --format="%(raw:size)" | sed "s/^/$SQ/;s/$/$SQ/" >expect && - git for-each-ref --format="%(raw:size)" --shell >actual && - test_cmp expect actual -' - -test_expect_success 'for-each-ref --format compare with cat-file --batch' ' - git rev-parse refs/mytrees/first | git cat-file --batch >expected && - git for-each-ref --format="%(objectname) %(objecttype) %(objectsize) -%(raw)" refs/mytrees/first >actual && - test_cmp expected actual -' - -test_expect_success 'verify sorts with contents:size' ' - cat >expect <<-\EOF && - refs/heads/main - refs/heads/newtag - refs/heads/ambiguous - EOF - git for-each-ref --format="%(refname)" \ - --sort=contents:size refs/heads/ >actual && - test_cmp expect actual -' - -test_expect_success 'set up multiple-sort tags' ' - for when in 100000 200000 - do - for email in user1 user2 - do - for ref in ref1 ref2 - do - GIT_COMMITTER_DATE="@$when +0000" \ - GIT_COMMITTER_EMAIL="$email@example.com" \ - git tag -m "tag $ref-$when-$email" \ - multi-$ref-$when-$email || return 1 - done - done - done -' - -test_expect_success 'Verify sort with multiple keys' ' - cat >expected <<-\EOF && - 100000 refs/tags/multi-ref2-100000-user1 - 100000 refs/tags/multi-ref1-100000-user1 - 100000 refs/tags/multi-ref2-100000-user2 - 100000 refs/tags/multi-ref1-100000-user2 - 200000 refs/tags/multi-ref2-200000-user1 - 200000 refs/tags/multi-ref1-200000-user1 - 200000 refs/tags/multi-ref2-200000-user2 - 200000 refs/tags/multi-ref1-200000-user2 - EOF - git for-each-ref \ - --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ - --sort=-refname \ - --sort=taggeremail \ - --sort=taggerdate \ - "refs/tags/multi-*" >actual && - test_cmp expected actual -' - -test_expect_success 'equivalent sorts fall back on refname' ' - cat >expected <<-\EOF && - 100000 refs/tags/multi-ref1-100000-user1 - 100000 refs/tags/multi-ref1-100000-user2 - 100000 refs/tags/multi-ref2-100000-user1 - 100000 refs/tags/multi-ref2-100000-user2 - 200000 refs/tags/multi-ref1-200000-user1 - 200000 refs/tags/multi-ref1-200000-user2 - 200000 refs/tags/multi-ref2-200000-user1 - 200000 refs/tags/multi-ref2-200000-user2 - EOF - git for-each-ref \ - --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ - --sort=taggerdate \ - "refs/tags/multi-*" >actual && - test_cmp expected actual -' - -test_expect_success '--no-sort cancels the previous sort keys' ' - cat >expected <<-\EOF && - 100000 refs/tags/multi-ref1-100000-user1 - 100000 refs/tags/multi-ref1-100000-user2 - 100000 refs/tags/multi-ref2-100000-user1 - 100000 refs/tags/multi-ref2-100000-user2 - 200000 refs/tags/multi-ref1-200000-user1 - 200000 refs/tags/multi-ref1-200000-user2 - 200000 refs/tags/multi-ref2-200000-user1 - 200000 refs/tags/multi-ref2-200000-user2 - EOF - git for-each-ref \ - --format="%(taggerdate:unix) %(taggeremail) %(refname)" \ - --sort=-refname \ - --sort=taggeremail \ - --no-sort \ - --sort=taggerdate \ - "refs/tags/multi-*" >actual && - test_cmp expected actual -' - -test_expect_success '--no-sort without subsequent --sort prints expected refs' ' - cat >expected <<-\EOF && - refs/tags/multi-ref1-100000-user1 - refs/tags/multi-ref1-100000-user2 - refs/tags/multi-ref1-200000-user1 - refs/tags/multi-ref1-200000-user2 - refs/tags/multi-ref2-100000-user1 - refs/tags/multi-ref2-100000-user2 - refs/tags/multi-ref2-200000-user1 - refs/tags/multi-ref2-200000-user2 - EOF - - # Sort the results with `sort` for a consistent comparison against - # expected - git for-each-ref \ - --format="%(refname)" \ - --no-sort \ - "refs/tags/multi-*" | sort >actual && - test_cmp expected actual -' - -test_expect_success 'set up custom date sorting' ' - # Dates: - # - Wed Feb 07 2024 21:34:20 +0000 - # - Tue Dec 14 1999 00:05:22 +0000 - # - Fri Jun 04 2021 11:26:51 +0000 - # - Mon Jan 22 2007 16:44:01 GMT+0000 - i=1 && - for when in 1707341660 945129922 1622806011 1169484241 - do - GIT_COMMITTER_DATE="@$when +0000" \ - GIT_COMMITTER_EMAIL="user@example.com" \ - git tag -m "tag $when" custom-dates-$i && - i=$(($i+1)) || return 1 - done -' - -test_expect_success 'sort by date defaults to full timestamp' ' - cat >expected <<-\EOF && - 945129922 refs/tags/custom-dates-2 - 1169484241 refs/tags/custom-dates-4 - 1622806011 refs/tags/custom-dates-3 - 1707341660 refs/tags/custom-dates-1 - EOF - - git for-each-ref \ - --format="%(creatordate:unix) %(refname)" \ - --sort=creatordate \ - "refs/tags/custom-dates-*" >actual && - test_cmp expected actual -' - -test_expect_success 'sort by custom date format' ' - cat >expected <<-\EOF && - 00:05:22 refs/tags/custom-dates-2 - 11:26:51 refs/tags/custom-dates-3 - 16:44:01 refs/tags/custom-dates-4 - 21:34:20 refs/tags/custom-dates-1 - EOF - - git for-each-ref \ - --format="%(creatordate:format:%H:%M:%S) %(refname)" \ - --sort="creatordate:format:%H:%M:%S" \ - "refs/tags/custom-dates-*" >actual && - test_cmp expected actual -' - -test_expect_success 'do not dereference NULL upon %(HEAD) on unborn branch' ' - test_when_finished "git checkout main" && - git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual && - sed -e "s/^\* / /" actual >expect && - git checkout --orphan orphaned-branch && - git for-each-ref --format="%(HEAD) %(refname:short)" refs/heads/ >actual && - test_cmp expect actual -' - -cat >trailers < -Signed-off-by: A U Thor -[ v2 updated patch description ] -Acked-by: A U Thor - -EOF - -unfold () { - perl -0pe 's/\n\s+/ /g' -} - -test_expect_success 'set up trailers for next test' ' - echo "Some contents" > two && - git add two && - git commit -F - <<-EOF - trailers: this commit message has trailers - - Some message contents - - $(cat trailers) - EOF -' - -test_trailer_option () { - if test "$#" -eq 3 - then - prereq="$1" - shift - fi && - title=$1 option=$2 - cat >expect - test_expect_success $prereq "$title" ' - git for-each-ref --format="%($option)" refs/heads/main >actual && - test_cmp expect actual && - git for-each-ref --format="%(contents:$option)" refs/heads/main >actual && - test_cmp expect actual - ' -} - -test_trailer_option PERL_TEST_HELPERS '%(trailers:unfold) unfolds trailers' \ - 'trailers:unfold' <<-EOF - $(unfold - - EOF - -test_trailer_option '%(trailers:key=foo) is case insensitive' \ - 'trailers:key=SiGned-oFf-bY' <<-EOF - Signed-off-by: A U Thor - - EOF - -test_trailer_option '%(trailers:key=foo:) trailing colon also works' \ - 'trailers:key=Signed-off-by:' <<-EOF - Signed-off-by: A U Thor - - EOF - -test_trailer_option '%(trailers:key=foo) multiple keys' \ - 'trailers:key=Reviewed-by:,key=Signed-off-by' <<-EOF - Reviewed-by: A U Thor - Signed-off-by: A U Thor - - EOF - -test_trailer_option '%(trailers:key=nonexistent) becomes empty' \ - 'trailers:key=Shined-off-by:' <<-EOF - - EOF - -test_trailer_option '%(trailers:key=foo) handles multiple lines even if folded' \ - 'trailers:key=Acked-by' <<-EOF - $(grep -v patch.description - $(grep patch.description - - EOF - -test_trailer_option '%(trailers:separator) changes separator' \ - 'trailers:separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF - Reviewed-by: A U Thor ,Signed-off-by: A U Thor - EOF - -test_trailer_option '%(trailers:key_value_separator) changes key-value separator' \ - 'trailers:key_value_separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF - Reviewed-by,A U Thor - Signed-off-by,A U Thor - - EOF - -test_trailer_option '%(trailers:separator,key_value_separator) changes both separators' \ - 'trailers:separator=%x2C,key_value_separator=%x2C,key=Reviewed-by,key=Signed-off-by:' <<-EOF - Reviewed-by,A U Thor ,Signed-off-by,A U Thor - EOF - -test_expect_success 'multiple %(trailers) use their own options' ' - git tag -F - tag-with-trailers <<-\EOF && - body - - one: foo - one: bar - two: baz - two: qux - EOF - t1="%(trailers:key=one,key_value_separator=W,separator=X)" && - t2="%(trailers:key=two,key_value_separator=Y,separator=Z)" && - git for-each-ref --format="$t1%0a$t2" refs/tags/tag-with-trailers >actual && - cat >expect <<-\EOF && - oneWfooXoneWbar - twoYbazZtwoYqux - EOF - test_cmp expect actual -' - -test_failing_trailer_option () { - title=$1 option=$2 - cat >expect - test_expect_success "$title" ' - # error message cannot be checked under i18n - test_must_fail git for-each-ref --format="%($option)" refs/heads/main 2>actual && - test_cmp expect actual && - test_must_fail git for-each-ref --format="%(contents:$option)" refs/heads/main 2>actual && - test_cmp expect actual - ' -} - -test_failing_trailer_option '%(trailers) rejects unknown trailers arguments' \ - 'trailers:unsupported' <<-\EOF - fatal: unknown %(trailers) argument: unsupported - EOF - -test_failing_trailer_option '%(trailers:key) without value is error' \ - 'trailers:key' <<-\EOF - fatal: expected %(trailers:key=) - EOF - -test_expect_success 'if arguments, %(contents:trailers) shows error if colon is missing' ' - cat >expect <<-EOF && - fatal: unrecognized %(contents) argument: trailersonly - EOF - test_must_fail git for-each-ref --format="%(contents:trailersonly)" 2>actual && - test_cmp expect actual -' - -test_expect_success 'basic atom: head contents:trailers' ' - git for-each-ref --format="%(contents:trailers)" refs/heads/main >actual && - sanitize_pgp actual.clean && - # git for-each-ref ends with a blank line - cat >expect <<-EOF && - $(cat trailers) - - EOF - test_cmp expect actual.clean -' - -test_expect_success 'basic atom: rest must fail' ' - test_must_fail git for-each-ref --format="%(rest)" refs/heads/main -' - -test_expect_success 'HEAD atom does not take arguments' ' - test_must_fail git for-each-ref --format="%(HEAD:foo)" 2>err && - echo "fatal: %(HEAD) does not take arguments" >expect && - test_cmp expect err -' - -test_expect_success 'subject atom rejects unknown arguments' ' - test_must_fail git for-each-ref --format="%(subject:foo)" 2>err && - echo "fatal: unrecognized %(subject) argument: foo" >expect && - test_cmp expect err -' - -test_expect_success 'refname atom rejects unknown arguments' ' - test_must_fail git for-each-ref --format="%(refname:foo)" 2>err && - echo "fatal: unrecognized %(refname) argument: foo" >expect && - test_cmp expect err -' - -test_expect_success 'trailer parsing not fooled by --- line' ' - git commit --allow-empty -F - <<-\EOF && - this is the subject - - This is the body. The message has a "---" line which would confuse a - message+patch parser. But here we know we have only a commit message, - so we get it right. - - trailer: wrong - --- - This is more body. - - trailer: right - EOF - - { - echo "trailer: right" && - echo - } >expect && - git for-each-ref --format="%(trailers)" refs/heads/main >actual && - test_cmp expect actual -' - -test_expect_success 'Add symbolic ref for the following tests' ' - git symbolic-ref refs/heads/sym refs/heads/main -' - -cat >expected <actual && - test_cmp expected actual -' - -cat >expected <actual && - test_cmp expected actual -' - -cat >expected < actual && - git for-each-ref --format="%(symref:lstrip=-2)" refs/heads/sym >> actual && - test_cmp expected actual && - - git for-each-ref --format="%(symref:strip=2)" refs/heads/sym > actual && - git for-each-ref --format="%(symref:strip=-2)" refs/heads/sym >> actual && - test_cmp expected actual -' - -cat >expected < actual && - git for-each-ref --format="%(symref:rstrip=-2)" refs/heads/sym >> actual && - test_cmp expected actual -' - -test_expect_success ':remotename and :remoteref' ' - git init remote-tests && - ( - cd remote-tests && - test_commit initial && - git branch -M main && - git remote add from fifth.coffee:blub && - git config branch.main.remote from && - git config branch.main.merge refs/heads/stable && - git remote add to southridge.audio:repo && - git config remote.to.push "refs/heads/*:refs/heads/pushed/*" && - git config branch.main.pushRemote to && - for pair in "%(upstream)=refs/remotes/from/stable" \ - "%(upstream:remotename)=from" \ - "%(upstream:remoteref)=refs/heads/stable" \ - "%(push)=refs/remotes/to/pushed/main" \ - "%(push:remotename)=to" \ - "%(push:remoteref)=refs/heads/pushed/main" - do - echo "${pair#*=}" >expect && - git for-each-ref --format="${pair%=*}" \ - refs/heads/main >actual && - test_cmp expect actual || exit 1 - done && - git branch push-simple && - git config branch.push-simple.pushRemote from && - actual="$(git for-each-ref \ - --format="%(push:remotename),%(push:remoteref)" \ - refs/heads/push-simple)" && - test from, = "$actual" - ) -' - -test_expect_success 'for-each-ref --ignore-case ignores case' ' - git for-each-ref --format="%(refname)" refs/heads/MAIN >actual && - test_must_be_empty actual && - - echo refs/heads/main >expect && - git for-each-ref --format="%(refname)" --ignore-case \ - refs/heads/MAIN >actual && - test_cmp expect actual -' - -test_expect_success 'for-each-ref --omit-empty works' ' - git for-each-ref --format="%(refname)" >actual && - test_line_count -gt 1 actual && - git for-each-ref --format="%(if:equals=refs/heads/main)%(refname)%(then)%(refname)%(end)" --omit-empty >actual && - echo refs/heads/main >expect && - test_cmp expect actual -' - -test_expect_success 'for-each-ref --ignore-case works on multiple sort keys' ' - # name refs numerically to avoid case-insensitive filesystem conflicts - nr=0 && - for email in a A b B - do - for subject in a A b B - do - GIT_COMMITTER_EMAIL="$email@example.com" \ - git tag -m "tag $subject" icase-$(printf %02d $nr) && - nr=$((nr+1))|| - return 1 - done - done && - git for-each-ref --ignore-case \ - --format="%(taggeremail) %(subject) %(refname)" \ - --sort=refname \ - --sort=subject \ - --sort=taggeremail \ - refs/tags/icase-* >actual && - cat >expect <<-\EOF && - tag a refs/tags/icase-00 - tag A refs/tags/icase-01 - tag a refs/tags/icase-04 - tag A refs/tags/icase-05 - tag b refs/tags/icase-02 - tag B refs/tags/icase-03 - tag b refs/tags/icase-06 - tag B refs/tags/icase-07 - tag a refs/tags/icase-08 - tag A refs/tags/icase-09 - tag a refs/tags/icase-12 - tag A refs/tags/icase-13 - tag b refs/tags/icase-10 - tag B refs/tags/icase-11 - tag b refs/tags/icase-14 - tag B refs/tags/icase-15 - EOF - test_cmp expect actual -' - -test_expect_success 'for-each-ref reports broken tags' ' - git tag -m "good tag" broken-tag-good HEAD && - git cat-file tag broken-tag-good >good && - sed s/commit/blob/ bad && - bad=$(git hash-object -w -t tag bad) && - git update-ref refs/tags/broken-tag-bad $bad && - test_must_fail git for-each-ref --format="%(*objectname)" \ - refs/tags/broken-tag-* -' - -test_expect_success 'set up tag with signature and no blank lines' ' - git tag -F - fake-sig-no-blanks <<-\EOF - this is the subject - -----BEGIN PGP SIGNATURE----- - not a real signature, but we just care about the - subject/body parsing. It is important here that - there are no blank lines in the signature. - -----END PGP SIGNATURE----- - EOF -' - -test_atom refs/tags/fake-sig-no-blanks contents:subject 'this is the subject' -test_atom refs/tags/fake-sig-no-blanks contents:body '' -test_atom refs/tags/fake-sig-no-blanks contents:signature "$sig" - -test_expect_success 'set up tag with CRLF signature' ' - append_cr <<-\EOF | - this is the subject - -----BEGIN PGP SIGNATURE----- - - not a real signature, but we just care about - the subject/body parsing. It is important here - that there is a blank line separating this - from the signature header. - -----END PGP SIGNATURE----- - EOF - git tag -F - --cleanup=verbatim fake-sig-crlf -' - -test_atom refs/tags/fake-sig-crlf contents:subject 'this is the subject' -test_atom refs/tags/fake-sig-crlf contents:body '' - -# CRLF is retained in the signature, so we have to pass our expected value -# through append_cr. But test_atom requires a shell string, which means command -# substitution, and the shell will strip trailing newlines from the output of -# the substitution. Hack around it by adding and then removing a dummy line. -sig_crlf="$(printf "%s" "$sig" | append_cr; echo dummy)" -sig_crlf=${sig_crlf%dummy} -test_atom refs/tags/fake-sig-crlf contents:signature "$sig_crlf" - -test_expect_success 'set up tag with signature and trailers' ' - git tag -F - fake-sig-trailer <<-\EOF - this is the subject - - this is the body - - My-Trailer: foo - -----BEGIN PGP SIGNATURE----- - - not a real signature, but we just care about the - subject/body/trailer parsing. - -----END PGP SIGNATURE----- - EOF -' - -# use "separator=" here to suppress the terminating newline -test_atom refs/tags/fake-sig-trailer trailers:separator= 'My-Trailer: foo' - -test_expect_success 'git for-each-ref --stdin: empty' ' - >in && - git for-each-ref --format="%(refname)" --stdin actual && - git for-each-ref --format="%(refname)" >expect && - test_cmp expect actual -' - -test_expect_success 'git for-each-ref --stdin: fails if extra args' ' - >in && - test_must_fail git for-each-ref --format="%(refname)" \ - --stdin refs/heads/extra err && - grep "unknown arguments supplied with --stdin" err -' - -test_expect_success 'git for-each-ref --stdin: matches' ' - cat >in <<-EOF && - refs/tags/multi* - refs/heads/amb* - EOF - - cat >expect <<-EOF && - refs/heads/ambiguous - refs/tags/multi-ref1-100000-user1 - refs/tags/multi-ref1-100000-user2 - refs/tags/multi-ref1-200000-user1 - refs/tags/multi-ref1-200000-user2 - refs/tags/multi-ref2-100000-user1 - refs/tags/multi-ref2-100000-user2 - refs/tags/multi-ref2-200000-user1 - refs/tags/multi-ref2-200000-user2 - refs/tags/multiline - EOF - - git for-each-ref --format="%(refname)" --stdin actual && - test_cmp expect actual -' - -test_expect_success 'git for-each-ref with non-existing refs' ' - cat >in <<-EOF && - refs/heads/this-ref-does-not-exist - refs/tags/bogus - EOF - - git for-each-ref --format="%(refname)" --stdin actual && - test_must_be_empty actual && - - xargs git for-each-ref --format="%(refname)" actual && - test_must_be_empty actual -' - -test_expect_success 'git for-each-ref with nested tags' ' - git tag -am "Normal tag" nested/base HEAD && - git tag -am "Nested tag" nested/nest1 refs/tags/nested/base && - git tag -am "Double nested tag" nested/nest2 refs/tags/nested/nest1 && - - head_oid="$(git rev-parse HEAD)" && - base_tag_oid="$(git rev-parse refs/tags/nested/base)" && - nest1_tag_oid="$(git rev-parse refs/tags/nested/nest1)" && - nest2_tag_oid="$(git rev-parse refs/tags/nested/nest2)" && - - cat >expect <<-EOF && - refs/tags/nested/base $base_tag_oid tag $head_oid commit - refs/tags/nested/nest1 $nest1_tag_oid tag $head_oid commit - refs/tags/nested/nest2 $nest2_tag_oid tag $head_oid commit - EOF - - git for-each-ref \ - --format="%(refname) %(objectname) %(objecttype) %(*objectname) %(*objecttype)" \ - refs/tags/nested/ >actual && - test_cmp expect actual -' - -test_expect_success 'is-base atom with non-commits' ' - git for-each-ref --format="%(is-base:HEAD) %(refname)" >out 2>err && - grep "(HEAD) refs/heads/main" out && - - test_line_count = 2 err && - grep "error: object .* is a commit, not a blob" err && - grep "error: bad tag pointer to" err -' - -GRADE_FORMAT="%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" -TRUSTLEVEL_FORMAT="%(signature:trustlevel)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)" - -test_expect_success GPG 'setup for signature atom using gpg' ' - git checkout -b signed && - - test_when_finished "test_unconfig commit.gpgSign" && - - echo "1" >file && - git add file && - test_tick && - git commit -S -m "file: 1" && - git tag first-signed && - - echo "2" >file && - test_tick && - git commit -a -m "file: 2" && - git tag second-unsigned && - - git config commit.gpgSign 1 && - echo "3" >file && - test_tick && - git commit -a --no-gpg-sign -m "file: 3" && - git tag third-unsigned && - - test_tick && - git rebase -f HEAD^^ && git tag second-signed HEAD^ && - git tag third-signed && - - echo "4" >file && - test_tick && - git commit -a -SB7227189 -m "file: 4" && - git tag fourth-signed && - - echo "5" >file && - test_tick && - git commit -a --no-gpg-sign -m "file: 5" && - git tag fifth-unsigned && - - echo "6" >file && - test_tick && - git commit -a --no-gpg-sign -m "file: 6" && - - test_tick && - git rebase -f HEAD^^ && - git tag fifth-signed HEAD^ && - git tag sixth-signed && - - echo "7" >file && - test_tick && - git commit -a --no-gpg-sign -m "file: 7" && - git tag seventh-unsigned -' - -test_expect_success GPGSSH 'setup for signature atom using ssh' ' - test_when_finished "test_unconfig gpg.format user.signingkey" && - - test_config gpg.format ssh && - test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" && - echo "8" >file && - test_tick && - git add file && - git commit -S -m "file: 8" && - git tag eighth-signed-ssh -' - -test_expect_success GPG2 'bare signature atom' ' - git verify-commit first-signed 2>expect && - echo >>expect && - git for-each-ref refs/tags/first-signed \ - --format="%(signature)" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show good signature with custom format' ' - git verify-commit first-signed && - cat >expect <<-\EOF && - G - 13B6F51ECDDE430D - C O Mitter - 73D758744BE721698EC54E8713B6F51ECDDE430D - 73D758744BE721698EC54E8713B6F51ECDDE430D - EOF - git for-each-ref refs/tags/first-signed \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' -test_expect_success GPGSSH 'show good signature with custom format with ssh' ' - test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" && - FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") && - cat >expect.tmpl <<-\EOF && - G - FINGERPRINT - principal with number 1 - FINGERPRINT - - EOF - sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect && - git for-each-ref refs/tags/eighth-signed-ssh \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'signature atom with grade option and bad signature' ' - git cat-file commit third-signed >raw && - sed -e "s/^file: 3/file: 3 forged/" raw >forged1 && - FORGED1=$(git hash-object -w -t commit forged1) && - git update-ref refs/tags/third-signed "$FORGED1" && - test_must_fail git verify-commit "$FORGED1" && - - cat >expect <<-\EOF && - B - 13B6F51ECDDE430D - C O Mitter - - - EOF - git for-each-ref refs/tags/third-signed \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show untrusted signature with custom format' ' - cat >expect <<-\EOF && - U - 65A0EEA02E30CAD7 - Eris Discordia - F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7 - D4BE22311AD3131E5EDA29A461092E85B7227189 - EOF - git for-each-ref refs/tags/fourth-signed \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show untrusted signature with undefined trust level' ' - cat >expect <<-\EOF && - undefined - 65A0EEA02E30CAD7 - Eris Discordia - F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7 - D4BE22311AD3131E5EDA29A461092E85B7227189 - EOF - git for-each-ref refs/tags/fourth-signed \ - --format="$TRUSTLEVEL_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show untrusted signature with ultimate trust level' ' - cat >expect <<-\EOF && - ultimate - 13B6F51ECDDE430D - C O Mitter - 73D758744BE721698EC54E8713B6F51ECDDE430D - 73D758744BE721698EC54E8713B6F51ECDDE430D - EOF - git for-each-ref refs/tags/sixth-signed \ - --format="$TRUSTLEVEL_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show unknown signature with custom format' ' - cat >expect <<-\EOF && - E - 13B6F51ECDDE430D - - - - EOF - GNUPGHOME="$GNUPGHOME_NOT_USED" git for-each-ref \ - refs/tags/sixth-signed --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' - -test_expect_success GPG 'show lack of signature with custom format' ' - cat >expect <<-\EOF && - N - - - - - EOF - git for-each-ref refs/tags/seventh-unsigned \ - --format="$GRADE_FORMAT" >actual && - test_cmp expect actual -' +. "$TEST_DIRECTORY"/for-each-ref-tests.sh test_done From fed66d91c0fce8a1911b24ad79ed9f10d411874e Mon Sep 17 00:00:00 2001 From: Meet Soni Date: Tue, 5 Aug 2025 14:57:58 +0530 Subject: [PATCH 06/13] t: add test for git refs list subcommand Add a test script, `t/t1461-refs-list.sh`, for the new `git refs list` command. This script acts as a simple driver, leveraging the shared test library created in the preceding commit. It works by overriding the `$git_for_each_ref` variable to "git refs list" and then sourcing the shared library (`t/for-each-ref-tests.sh`). This approach ensures that `git refs list` is tested against the entire comprehensive test suite of `git for-each-ref`, verifying that it acts as a compatible drop-in replacement. Mentored-by: Patrick Steinhardt Mentored-by: shejialuo Mentored-by: Karthik Nayak Signed-off-by: Meet Soni Signed-off-by: Junio C Hamano --- t/meson.build | 1 + t/t1461-refs-list.sh | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100755 t/t1461-refs-list.sh diff --git a/t/meson.build b/t/meson.build index bbeba1a8d50e1b..23a370295891cf 100644 --- a/t/meson.build +++ b/t/meson.build @@ -208,6 +208,7 @@ integration_tests = [ 't1450-fsck.sh', 't1451-fsck-buffer.sh', 't1460-refs-migrate.sh', + 't1461-refs-list.sh', 't1500-rev-parse.sh', 't1501-work-tree.sh', 't1502-rev-parse-parseopt.sh', diff --git a/t/t1461-refs-list.sh b/t/t1461-refs-list.sh new file mode 100755 index 00000000000000..36e3d81e5970e7 --- /dev/null +++ b/t/t1461-refs-list.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +test_description='git refs list tests' + +. ./test-lib.sh + +git_for_each_ref='git refs list' +. "$TEST_DIRECTORY"/for-each-ref-tests.sh From b55e6d36ebce69136559add8fffd1a65df231518 Mon Sep 17 00:00:00 2001 From: Lidong Yan Date: Fri, 8 Aug 2025 11:30:19 +0800 Subject: [PATCH 07/13] diff: ensure consistent diff behavior with ignore options In git-diff, options like `-w` and `-I`, two files are considered equivalent under the specified "ignore" rules, even when they are not bit-for-bit identical. For options like `--raw`, `--name-status`, and `--name-only`, git-diff deliberately compares only the SHA values to determine whether two files are equivalent, for performance reasons. As a result, a file shown in `git diff --name-status` may not appear in `git diff --patch`. To quickly determine whether two files are equivalent, add a helper function diff_flush_patch_quietly() in diff.c. Add `.dry_run` field in `struct diff_options`. When `.dry_run` is true, builtin_diff() returns immediately upon finding any change. Call diff_flush_patch_quietly() to determine if we should flush `--raw`, `--name-only` or `--name-status` output. Signed-off-by: Jeff King Signed-off-by: Lidong Yan Signed-off-by: Junio C Hamano --- diff.c | 64 +++++++++++++++++++++++++++++--------- diff.h | 2 ++ t/t4013-diff-various.sh | 13 ++++++++ t/t4015-diff-whitespace.sh | 8 ++--- xdiff-interface.h | 6 ++-- 5 files changed, 70 insertions(+), 23 deletions(-) diff --git a/diff.c b/diff.c index dca87e164fb615..cb04a9a6f26f5c 100644 --- a/diff.c +++ b/diff.c @@ -2444,6 +2444,15 @@ static int fn_out_consume(void *priv, char *line, unsigned long len) return 0; } +static int quick_consume(void *priv, char *line UNUSED, unsigned long len UNUSED) +{ + struct emit_callback *ecbdata = priv; + struct diff_options *o = ecbdata->opt; + + o->found_changes = 1; + return 1; +} + static void pprint_rename(struct strbuf *name, const char *a, const char *b) { const char *old_name = a; @@ -3759,8 +3768,21 @@ static void builtin_diff(const char *name_a, if (o->word_diff) init_diff_words_data(&ecbdata, o, one, two); - if (xdi_diff_outf(&mf1, &mf2, NULL, fn_out_consume, - &ecbdata, &xpp, &xecfg)) + if (o->dry_run) { + /* + * Unlike the !dry_run case, we need to ignore the + * return value from xdi_diff_outf() here, because + * xdi_diff_outf() takes non-zero return from its + * callback function as a sign of error and returns + * early (which is why we return non-zero from our + * callback, quick_consume()). Unfortunately, + * xdi_diff_outf() signals an error by returning + * non-zero. + */ + xdi_diff_outf(&mf1, &mf2, NULL, quick_consume, + &ecbdata, &xpp, &xecfg); + } else if (xdi_diff_outf(&mf1, &mf2, NULL, fn_out_consume, + &ecbdata, &xpp, &xecfg)) die("unable to generate diff for %s", one->path); if (o->word_diff) free_diff_words_data(&ecbdata); @@ -6150,6 +6172,22 @@ static void diff_flush_patch(struct diff_filepair *p, struct diff_options *o) run_diff(p, o); } +/* return 1 if any change is found; otherwise, return 0 */ +static int diff_flush_patch_quietly(struct diff_filepair *p, struct diff_options *o) +{ + int saved_dry_run = o->dry_run; + int saved_found_changes = o->found_changes; + int ret; + + o->dry_run = 1; + o->found_changes = 0; + diff_flush_patch(p, o); + ret = o->found_changes; + o->dry_run = saved_dry_run; + o->found_changes |= saved_found_changes; + return ret; +} + static void diff_flush_stat(struct diff_filepair *p, struct diff_options *o, struct diffstat_t *diffstat) { @@ -6778,8 +6816,15 @@ void diff_flush(struct diff_options *options) DIFF_FORMAT_CHECKDIFF)) { for (i = 0; i < q->nr; i++) { struct diff_filepair *p = q->queue[i]; - if (check_pair_status(p)) - flush_one_pair(p, options); + + if (!check_pair_status(p)) + continue; + + if (options->flags.diff_from_contents && + !diff_flush_patch_quietly(p, options)) + continue; + + flush_one_pair(p, options); } separator++; } @@ -6831,19 +6876,10 @@ void diff_flush(struct diff_options *options) if (output_format & DIFF_FORMAT_NO_OUTPUT && options->flags.exit_with_status && options->flags.diff_from_contents) { - /* - * run diff_flush_patch for the exit status. setting - * options->file to /dev/null should be safe, because we - * aren't supposed to produce any output anyway. - */ - diff_free_file(options); - options->file = xfopen("/dev/null", "w"); - options->close_file = 1; - options->color_moved = 0; for (i = 0; i < q->nr; i++) { struct diff_filepair *p = q->queue[i]; if (check_pair_status(p)) - diff_flush_patch(p, options); + diff_flush_patch_quietly(p, options); if (options->found_changes) break; } diff --git a/diff.h b/diff.h index 62e5768a9a379e..91b3e1c5cf7914 100644 --- a/diff.h +++ b/diff.h @@ -400,6 +400,8 @@ struct diff_options { #define COLOR_MOVED_WS_ERROR (1<<0) unsigned color_moved_ws_handling; + bool dry_run; + struct repository *repo; struct strmap *additional_path_headers; diff --git a/t/t4013-diff-various.sh b/t/t4013-diff-various.sh index 8ebd170451d025..cfeec239e0946c 100755 --- a/t/t4013-diff-various.sh +++ b/t/t4013-diff-various.sh @@ -648,6 +648,19 @@ test_expect_success 'diff -I: detect malformed regex' ' test_grep "invalid regex given to -I: " error ' +test_expect_success 'diff -I: ignore matching file' ' + test_when_finished "git rm -f file1" && + test_seq 50 >file1 && + git add file1 && + test_seq 50 | sed -e "s/13/ten and three/" -e "s/^[124-9].*/& /" >file1 && + + : >actual && + git diff --raw --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >>actual && + git diff --name-only --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >>actual && + git diff --name-status --ignore-blank-lines -I"ten.*e" -I"^[124-9]" >>actual && + test_grep ! "file1" actual +' + # check_prefix # check only lines with paths to avoid dependency on exact oid/contents check_prefix () { diff --git a/t/t4015-diff-whitespace.sh b/t/t4015-diff-whitespace.sh index 52e3e476ffa5a9..9de7f73f42b534 100755 --- a/t/t4015-diff-whitespace.sh +++ b/t/t4015-diff-whitespace.sh @@ -11,12 +11,8 @@ test_description='Test special whitespace in diff engine. . "$TEST_DIRECTORY"/lib-diff.sh for opt_res in --patch --quiet -s --stat --shortstat --dirstat=lines \ - --raw! --name-only! --name-status! + --raw --name-only --name-status do - opts=${opt_res%!} expect_failure= - test "$opts" = "$opt_res" || - expect_failure="test_expect_code 1" - test_expect_success "status with $opts (different)" ' echo foo >x && git add x && @@ -43,7 +39,7 @@ do echo foo >x && git add x && echo " foo" >x && - $expect_failure git diff -w $opts --exit-code x + git diff -w $opts --exit-code x ' done diff --git a/xdiff-interface.h b/xdiff-interface.h index 1ed430b622adf7..dfc55daddf46ed 100644 --- a/xdiff-interface.h +++ b/xdiff-interface.h @@ -28,9 +28,9 @@ * from an error internal to xdiff, xdiff itself will see that * non-zero return and translate it to -1. * - * See "diff_grep" in diffcore-pickaxe.c for a trick to work around - * this, i.e. using the "consume_callback_data" to note the desired - * early return. + * See "diff_grep" in diffcore-pickaxe.c and "quick_consume" in diff.c + * for a trick to work around this, i.e. using the "consume_callback_data" + * to note the desired early return. */ typedef int (*xdiff_emit_line_fn)(void *, char *, unsigned long); typedef void (*xdiff_emit_hunk_fn)(void *data, From e1d3d61a45bfdc5031d2066c0e4505ebd8145777 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sat, 9 Aug 2025 17:20:36 -0700 Subject: [PATCH 08/13] diff: --no-index should ignore the worktree The act of giving "--no-index" tells Git to pretend that the current directory is not under control of any Git index or repository, so even when you happen to be in a Git controlled working tree, where in that working tree should not matter. But the start-up sequence tries to discover the top of the working tree and chdir(2)'s there, even before Git passes control to the subcommand being run. When diff_no_index() starts running, it starts at a wrong (from the end-user's point of view who thinks "git diff --no-index" is merely a better version of GNU diff) directory, and the original directory the user started the command is at "prefix". Because the paths given from argv[] have already been adjusted to account for this path shuffling by prepending the prefix, and showing the resulting path by stripping the prefix, the effect of these nonsense operations (nonsense in the context of "--no-index", that is) is usually not observable. Except for special cases like "-", where it is not preprocessed by prepending the prefix. Instead of papering over by adding more special cases only to cater to the no-index codepath in the generic code, drive the diff machinery more faithfully to what is going on. If the user started "git diff --no-index" in directory X/Y/Z in a working tree controlled by Git, and the start up sequence of Git chdir(2)'ed up to directory X and left Y/Z in the prefix, revert the effect of the start up sequence by chdir'ing back to Y/Z and emptying the prefix. Reported-by: Gregoire Geis Helped-by: Ramsay Jones Signed-off-by: Junio C Hamano --- builtin/diff.c | 15 +++++++++++++++ t/t4053-diff-no-index.sh | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/builtin/diff.c b/builtin/diff.c index 9a89e25a982abb..0b23c41456837f 100644 --- a/builtin/diff.c +++ b/builtin/diff.c @@ -487,6 +487,21 @@ int cmd_diff(int argc, init_diff_ui_defaults(); repo_config(the_repository, git_diff_ui_config, NULL); + + /* + * If we are ignoring the fact that our current directory may + * be part of a working tree controlled by a Git repository to + * pretend to be a "better GNU diff", we should undo the + * effect of the setup code that did a chdir() to the top of + * the working tree. Where we came from is recorded in the + * prefix. + */ + if (no_index && prefix) { + if (chdir(prefix)) + die(_("cannot come back to cwd")); + prefix = NULL; + } + prefix = precompose_argv_prefix(argc, argv, prefix); repo_init_revisions(the_repository, &rev, prefix); diff --git a/t/t4053-diff-no-index.sh b/t/t4053-diff-no-index.sh index 01db9243abfe4f..44b4b13f5d90e3 100755 --- a/t/t4053-diff-no-index.sh +++ b/t/t4053-diff-no-index.sh @@ -26,6 +26,23 @@ test_expect_success 'git diff --no-index directories' ' test_line_count = 14 cnt ' +test_expect_success 'git diff --no-index with -' ' + cat >expect <<-\EOF && + diff --git a/- b/- + new file mode 100644 + --- /dev/null + +++ b/- + @@ -0,0 +1 @@ + +frotz + EOF + ( + cd a && + echo frotz | + test_expect_code 1 git diff --no-index /dev/null - >../actual + ) && + test_cmp expect actual +' + test_expect_success 'git diff --no-index relative path outside repo' ' ( cd repo && From 621ce9c1c6cfe5a6467ef62cb81992b3a318b70e Mon Sep 17 00:00:00 2001 From: Greg Hurrell Date: Mon, 11 Aug 2025 11:55:23 +0000 Subject: [PATCH 09/13] git-jump: make `diff` work with filenames containing spaces In diff.c, we output a trailing "\t" at the end of any filename that contains a space: case DIFF_SYMBOL_FILEPAIR_PLUS: meta = diff_get_color_opt(o, DIFF_METAINFO); reset = diff_get_color_opt(o, DIFF_RESET); fprintf(o->file, "%s%s+++ %s%s%s\n", diff_line_prefix(o), meta, line, reset, strchr(line, ' ') ? "\t" : ""); break; That is, for a file "foo.txt", `git diff --no-prefix` will emit: +++ foo.txt but for "foo bar.txt" it will emit: +++ foo bar.txt\t This in turn leads `git-jump` to produce a quickfix format like this: foo bar.txt\t:1:1:contents Because no "foo bar.txt\t" file actually exists on disk, opening it in Vim will just land the user in an empty buffer. This commit takes the simple approach of unconditionally stripping any trailing tab. Consider the following three examples: 1. For file "foo", Git will emit "foo". 2. For file "foo bar", Git will emit "foo bar\t". 3. For file "foo\t", Git will emit "\"foo\t\"". 4. For file "foo bar\t", Git will emit "\"foo bar\t\"". Before this commit, `git-jump` correctly handled only case "1". After this commit, `git-jump` correctly handles cases "1" and "2". In reality, these are the only cases people are going to run into with any regularity, and the other two are rare edge cases, which probably aren't worth the effort to support unless somebody actually complains about them. Signed-off-by: Greg Hurrell Signed-off-by: Junio C Hamano --- contrib/git-jump/git-jump | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/git-jump/git-jump b/contrib/git-jump/git-jump index 3f696759617beb..8d1d5d79a69854 100755 --- a/contrib/git-jump/git-jump +++ b/contrib/git-jump/git-jump @@ -44,7 +44,7 @@ open_editor() { mode_diff() { git diff --no-prefix --relative "$@" | perl -ne ' - if (m{^\+\+\+ (.*)}) { $file = $1 eq "/dev/null" ? undef : $1; next } + if (m{^\+\+\+ (.*?)\t?$}) { $file = $1 eq "/dev/null" ? undef : $1; next } defined($file) or next; if (m/^@@ .*?\+(\d+)/) { $line = $1; next } defined($line) or next; From 9a49aef8dcdf899e94cddab14eacc7118c611524 Mon Sep 17 00:00:00 2001 From: Ayush Chandekar Date: Mon, 11 Aug 2025 05:15:45 +0530 Subject: [PATCH 10/13] environment: remove the global variable 'merge_log_config' The global variable 'merge_log_config', set via the "merge.log" or "merge.summary" settings, is only used in 'cmd_fmt_merge_msg()' and 'cmd_merge()' to adjust the 'shortlog_len' variable. Remove 'merge_log_config' globally and localize it in 'cmd_fmt_merge_msg()' and 'cmd_merge()'. Set its value by passing it in 'fmt_merge_msg_config()' by passing its pointer to the function via the callback parameter. This change is part of an ongoing effort to eliminate global variables, improve modularity and help libify the codebase. Mentored-by: Christian Couder Mentored-by: Ghanshyam Thakkar Signed-off-by: Ayush Chandekar Signed-off-by: Junio C Hamano --- builtin/fmt-merge-msg.c | 3 ++- builtin/merge.c | 3 ++- environment.c | 1 - fmt-merge-msg.c | 10 ++++++---- fmt-merge-msg.h | 1 - 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 3b6aac2cf7faab..4b24de32fb499a 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -19,6 +19,7 @@ int cmd_fmt_merge_msg(int argc, const char *message = NULL; char *into_name = NULL; int shortlog_len = -1; + int merge_log_config = -1; struct option options[] = { { .type = OPTION_INTEGER, @@ -53,7 +54,7 @@ int cmd_fmt_merge_msg(int argc, int ret; struct fmt_merge_msg_opts opts; - git_config(fmt_merge_msg_config, NULL); + git_config(fmt_merge_msg_config, &merge_log_config); argc = parse_options(argc, argv, prefix, options, fmt_merge_msg_usage, 0); if (argc > 0) diff --git a/builtin/merge.c b/builtin/merge.c index ce90e52fe451d6..1c921b12f5b7de 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -1316,6 +1316,7 @@ int cmd_merge(int argc, struct commit_list *remoteheads = NULL, *p; void *branch_to_free; int orig_argc = argc; + int merge_log_config = -1; show_usage_with_options_if_asked(argc, argv, builtin_merge_usage, builtin_merge_options); @@ -1334,7 +1335,7 @@ int cmd_merge(int argc, skip_prefix(branch, "refs/heads/", &branch); init_diff_ui_defaults(); - git_config(git_merge_config, NULL); + git_config(git_merge_config, &merge_log_config); if (!branch || is_null_oid(&head_oid)) head_commit = NULL; diff --git a/environment.c b/environment.c index c61d773e7e8ff0..2babf53aa3f0ad 100644 --- a/environment.c +++ b/environment.c @@ -67,7 +67,6 @@ int grafts_keep_true_parents; int core_apply_sparse_checkout; int core_sparse_checkout_cone; int sparse_expect_files_outside_of_patterns; -int merge_log_config = -1; int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ unsigned long pack_size_limit_cfg; int max_allowed_tree_depth = diff --git a/fmt-merge-msg.c b/fmt-merge-msg.c index 501b5acdd44c22..6bd395bb65d48f 100644 --- a/fmt-merge-msg.c +++ b/fmt-merge-msg.c @@ -26,13 +26,15 @@ static struct string_list suppress_dest_patterns = STRING_LIST_INIT_DUP; int fmt_merge_msg_config(const char *key, const char *value, const struct config_context *ctx, void *cb) { + int *merge_log_config = cb; + if (!strcmp(key, "merge.log") || !strcmp(key, "merge.summary")) { int is_bool; - merge_log_config = git_config_bool_or_int(key, value, ctx->kvi, &is_bool); - if (!is_bool && merge_log_config < 0) + *merge_log_config = git_config_bool_or_int(key, value, ctx->kvi, &is_bool); + if (!is_bool && *merge_log_config < 0) return error("%s: negative length %s", key, value); - if (is_bool && merge_log_config) - merge_log_config = DEFAULT_MERGE_LOG_LEN; + if (is_bool && *merge_log_config) + *merge_log_config = DEFAULT_MERGE_LOG_LEN; } else if (!strcmp(key, "merge.branchdesc")) { use_branch_desc = git_config_bool(key, value); } else if (!strcmp(key, "merge.suppressdest")) { diff --git a/fmt-merge-msg.h b/fmt-merge-msg.h index 73ca3e44652204..c066d837610fa5 100644 --- a/fmt-merge-msg.h +++ b/fmt-merge-msg.h @@ -12,7 +12,6 @@ struct fmt_merge_msg_opts { const char *into_name; }; -extern int merge_log_config; int fmt_merge_msg_config(const char *key, const char *value, const struct config_context *ctx, void *cb); int fmt_merge_msg(struct strbuf *in, struct strbuf *out, From 22d421fed9cd5757a9da5a97e5b53ded54e93fe9 Mon Sep 17 00:00:00 2001 From: Ayush Chandekar Date: Mon, 11 Aug 2025 05:15:46 +0530 Subject: [PATCH 11/13] builtin/fmt-merge-msg: stop depending on 'the_repository' Refactor builtin/fmt-merge-msg.c to remove the dependancy on the global 'the_repository'. Remove the 'UNUSED' macro from the 'struct repository' parameter and replace 'git_config()' with 'repo_config()' so that configuration is read from the passed repository. Also, add a test to make sure that "git fmt-merge-msg -h" can be called outside a repository. Mentored-by: Christian Couder Mentored-by: Ghanshyam Thakkar Signed-off-by: Ayush Chandekar Signed-off-by: Junio C Hamano --- builtin/fmt-merge-msg.c | 5 ++--- t/t1517-outside-repo.sh | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c index 4b24de32fb499a..cf4273a52c2bae 100644 --- a/builtin/fmt-merge-msg.c +++ b/builtin/fmt-merge-msg.c @@ -1,4 +1,3 @@ -#define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" #include "config.h" #include "fmt-merge-msg.h" @@ -13,7 +12,7 @@ static const char * const fmt_merge_msg_usage[] = { int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { char *inpath = NULL; const char *message = NULL; @@ -54,7 +53,7 @@ int cmd_fmt_merge_msg(int argc, int ret; struct fmt_merge_msg_opts opts; - git_config(fmt_merge_msg_config, &merge_log_config); + repo_config(repo, fmt_merge_msg_config, &merge_log_config); argc = parse_options(argc, argv, prefix, options, fmt_merge_msg_usage, 0); if (argc > 0) diff --git a/t/t1517-outside-repo.sh b/t/t1517-outside-repo.sh index 6824581317411a..f6d3206cfe381f 100755 --- a/t/t1517-outside-repo.sh +++ b/t/t1517-outside-repo.sh @@ -114,4 +114,11 @@ test_expect_success 'update-server-info does not crash with -h' ' test_grep "[Uu]sage: git update-server-info " usage ' +test_expect_success 'fmt-merge-msg does not crash with -h' ' + test_expect_code 129 git fmt-merge-msg -h >usage && + test_grep "[Uu]sage: git fmt-merge-msg " usage && + test_expect_code 129 nongit git fmt-merge-msg -h >usage && + test_grep "[Uu]sage: git fmt-merge-msg " usage +' + test_done From 741f36c7d9c87648cc4ecb68eb9156fa50769e56 Mon Sep 17 00:00:00 2001 From: Knut Harald Ryager Date: Mon, 11 Aug 2025 22:04:32 +0000 Subject: [PATCH 12/13] docs: remove stray bracket from git-clone synopsis The synopsis section has an extra closing bracket, like this: [--filter=] [--also-filter-submodules]] The extra one is not the one at the end of this line; it is the one after "...=". The "--also-filter-submodules" option was added by f05da2b4 (clone, submodule: pass partial clone filters to submodules, 2022-02-04). Because it makes sense only when used with the "--filter=" option, these two options are enclosed in a pair of brackets. The extra one was added by 76880f05 (doc: git-clone: apply new documentation formatting guidelines, 2024-03-29) by mistake. Remove the extra and incorrect closing bracket, so that the line reads: [--filter= [--also-filter-submodules]] Signed-off-by: Knut Harald Ryager Signed-off-by: Junio C Hamano --- Documentation/git-clone.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index de8d8f58930ecf..c0d8e1e89779d5 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -16,7 +16,7 @@ git clone [--template=] [--depth ] [--[no-]single-branch] [--no-tags] [--recurse-submodules[=]] [--[no-]shallow-submodules] [--[no-]remote-submodules] [--jobs ] [--sparse] [--[no-]reject-shallow] - [--filter=] [--also-filter-submodules]] [--] + [--filter= [--also-filter-submodules]] [--] [] DESCRIPTION From 1fa68948c3d76328236cac73d2adf33c905bd8e3 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 22 Aug 2025 11:38:52 -0700 Subject: [PATCH 13/13] The second batch --- Documentation/RelNotes/2.52.0.adoc | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Documentation/RelNotes/2.52.0.adoc b/Documentation/RelNotes/2.52.0.adoc index 1919e039bd3401..f96aa8dc10cd74 100644 --- a/Documentation/RelNotes/2.52.0.adoc +++ b/Documentation/RelNotes/2.52.0.adoc @@ -4,6 +4,9 @@ Git v2.52 Release Notes UI, Workflows & Features ------------------------ + * The "list" subcommand of "git refs" acts as a front-end for + "git for-each-ref". + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -21,6 +24,7 @@ Performance, Internal Implementation, Development Support etc. contained wildcards; now they take advantage of the filter when they can. + Fixes since v2.51 ----------------- @@ -52,7 +56,26 @@ including security updates, are included in this release. been fixed. (merge f6ecb603ff en/ort-rename-fixes later to maint). + * "git jump" (in contrib/) fails to parse the diff header correctly + when a file has a space in its name, which has been corrected. + (merge 621ce9c1c6 gh/git-jump-pathname-with-sp later to maint). + + * "git diff --no-index" run inside a subdirectory under control of a + Git repository operated at the top of the working tree and stripped + the prefix from the output, and oddballs like "-" (stdin) did not + work correctly because of it. Correct the set-up by undoing what + the set-up sequence did to cwd and prefix. + (merge e1d3d61a45 jc/diff-no-index-in-subdir later to maint). + + * Various options to "git diff" that makes comparison ignore certain + aspects of the differences (like "space changes are ignored", + "differences in lines that match these regular expressions are + ignored") did not work well with "--name-only" and friends. + (merge b55e6d36eb ly/diff-name-only-with-diff-from-content later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 823d537fa7 kh/doc-git-log-markup-fix later to maint). (merge cf7efa4f33 rj/t6137-cygwin-fix later to maint). (merge 529a60a885 ua/t1517-short-help-tests later to maint). + (merge 22d421fed9 ac/deglobal-fmt-merge-log-config later to maint). + (merge 741f36c7d9 kr/clone-synopsis-fix later to maint).