From 9f4e170dfc3bd8cdd284f1c4411b25ce1d09737f Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Wed, 27 May 2026 15:50:00 +0000 Subject: [PATCH 01/21] pack-objects: call release_revisions() after cruft traversal enumerate_and_traverse_cruft_objects() initializes a rev_info on the stack but never calls release_revisions() afterwards. This is not visible on master but becomes a leak once the revision walking machinery uses dynamically allocated structures. Add the missing release_revisions() call. Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 1 + 1 file changed, 1 insertion(+) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 480cc0bd8c8d22..67025e86256cfd 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -4275,6 +4275,7 @@ static void enumerate_and_traverse_cruft_objects(struct string_list *fresh_packs traverse_commit_list(&revs, show_cruft_commit, show_cruft_object, NULL); stop_progress(&progress_state); + release_revisions(&revs); } static void read_cruft_objects(void) From d877b1af507a6aaf55e8643eb73277a30d3a800b Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Wed, 27 May 2026 15:50:01 +0000 Subject: [PATCH 02/21] revision: introduce rev_walk_mode to clarify get_revision_1() get_revision_1() dispatches to different walk strategies based on a combination of rev_info flags: reflog_info, topo_walk_info, and limited. These conditions are checked in multiple places within the function -- once to select the next commit, and again to decide how to expand parents -- and the two chains must stay in sync. Extract the mode selection into a rev_walk_mode enum and a small get_walk_mode() helper, resolved once at the top of get_revision_1(). Both dispatch sites now switch on the same mode variable, making it obvious that they agree and easier to verify that all modes are handled. No functional change. Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- revision.c | 62 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/revision.c b/revision.c index e1970b9c5d34ed..9d0fc696d09937 100644 --- a/revision.c +++ b/revision.c @@ -4327,22 +4327,48 @@ static void track_linear(struct rev_info *revs, struct commit *commit) revs->previous_parents = commit_list_copy(commit->parents); } +enum rev_walk_mode { + REV_WALK_REFLOG, + REV_WALK_TOPO, + REV_WALK_LIMITED, + REV_WALK_STREAMING, +}; + +static enum rev_walk_mode get_walk_mode(struct rev_info *revs) +{ + if (revs->reflog_info) + return REV_WALK_REFLOG; + if (revs->topo_walk_info) + return REV_WALK_TOPO; + if (revs->limited) + return REV_WALK_LIMITED; + return REV_WALK_STREAMING; +} + static struct commit *get_revision_1(struct rev_info *revs) { + enum rev_walk_mode mode = get_walk_mode(revs); + while (1) { struct commit *commit; - if (revs->reflog_info) + switch (mode) { + case REV_WALK_REFLOG: commit = next_reflog_entry(revs->reflog_info); - else if (revs->topo_walk_info) + break; + case REV_WALK_TOPO: commit = next_topo_commit(revs); - else + break; + case REV_WALK_LIMITED: + case REV_WALK_STREAMING: commit = pop_commit(&revs->commits); + break; + } if (!commit) return NULL; - if (revs->reflog_info) + if (mode == REV_WALK_REFLOG) commit->object.flags &= ~(ADDED | SEEN | SHOWN); /* @@ -4350,20 +4376,28 @@ static struct commit *get_revision_1(struct rev_info *revs) * the parents here. We also need to do the date-based limiting * that we'd otherwise have done in limit_list(). */ - if (!revs->limited) { - if (revs->max_age != -1 && - comparison_date(revs, commit) < revs->max_age) - continue; + if (mode != REV_WALK_LIMITED && + revs->max_age != -1 && + comparison_date(revs, commit) < revs->max_age) + continue; - if (revs->reflog_info) - try_to_simplify_commit(revs, commit); - else if (revs->topo_walk_info) - expand_topo_walk(revs, commit); - else if (process_parents(revs, commit, &revs->commits, NULL) < 0) { + switch (mode) { + case REV_WALK_REFLOG: + try_to_simplify_commit(revs, commit); + break; + case REV_WALK_TOPO: + expand_topo_walk(revs, commit); + break; + case REV_WALK_STREAMING: + if (process_parents(revs, commit, + &revs->commits, NULL) < 0) { if (!revs->ignore_missing_links) die("Failed to traverse parents of commit %s", - oid_to_hex(&commit->object.oid)); + oid_to_hex(&commit->object.oid)); } + break; + case REV_WALK_LIMITED: + break; } switch (simplify_commit(revs, commit)) { From dd4bc01c0a8fc871a68a5027ed5ac953fa47fc6e Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Wed, 27 May 2026 15:50:02 +0000 Subject: [PATCH 03/21] revision: use priority queue for non-limited streaming walks The streaming (non-limited) walk in get_revision_1() inserts newly discovered parent commits into a date-sorted queue via commit_list_insert_by_date(), which scans the linked list to find the insertion point -- O(w) per insert, where w is the width of the active walk frontier. Replace this with an O(log w) priority queue. Add a commit_queue field to rev_info alongside the existing commits linked list. The two representations are mutually exclusive: setup and external callers that need list access use the linked list, then get_revision_1() lazily drains it into the priority queue on first call. Add a REV_WALK_NO_WALK enum value to distinguish the no_walk case (which still uses the commit list) from the streaming case. The conversion function rev_info_commit_list_to_queue() is public so callers that know they will iterate can convert early. Combined with the limit_list() priority queue change already in master, this eliminates all O(w) sorted linked-list insertion from the revision walk machinery. Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- commit.c | 13 ------------- commit.h | 2 -- revision.c | 55 +++++++++++++++++++++++++++++------------------------- revision.h | 12 +++++++++++- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/commit.c b/commit.c index e3e7352e69682d..5112c7b2af31b1 100644 --- a/commit.c +++ b/commit.c @@ -729,19 +729,6 @@ void commit_list_free(struct commit_list *list) pop_commit(&list); } -struct commit_list * commit_list_insert_by_date(struct commit *item, struct commit_list **list) -{ - struct commit_list **pp = list; - struct commit_list *p; - while ((p = *pp) != NULL) { - if (p->item->date < item->date) { - break; - } - pp = &p->next; - } - return commit_list_insert(item, pp); -} - static int commit_list_compare_by_date(const struct commit_list *a, const struct commit_list *b) { diff --git a/commit.h b/commit.h index 58150045afafed..385492fbb1ecc5 100644 --- a/commit.h +++ b/commit.h @@ -191,8 +191,6 @@ int commit_list_contains(struct commit *item, struct commit_list **commit_list_append(struct commit *commit, struct commit_list **next); unsigned commit_list_count(const struct commit_list *l); -struct commit_list *commit_list_insert_by_date(struct commit *item, - struct commit_list **list); void commit_list_sort_by_date(struct commit_list **list); /* Shallow copy of the input list */ diff --git a/revision.c b/revision.c index 9d0fc696d09937..4bb3b16e43acb9 100644 --- a/revision.c +++ b/revision.c @@ -1116,7 +1116,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) } static int process_parents(struct rev_info *revs, struct commit *commit, - struct commit_list **list, struct prio_queue *queue) + struct prio_queue *queue) { struct commit_list *parent = commit->parents; unsigned pass_flags; @@ -1158,8 +1158,6 @@ static int process_parents(struct rev_info *revs, struct commit *commit, if (p->object.flags & SEEN) continue; p->object.flags |= (SEEN | NOT_USER_GIVEN); - if (list) - commit_list_insert_by_date(p, list); if (queue) prio_queue_put(queue, p); if (revs->exclude_first_parent_only) @@ -1207,8 +1205,6 @@ static int process_parents(struct rev_info *revs, struct commit *commit, p->object.flags |= pass_flags | CHILD_VISITED; if (!(p->object.flags & SEEN)) { p->object.flags |= (SEEN | NOT_USER_GIVEN); - if (list) - commit_list_insert_by_date(p, list); if (queue) prio_queue_put(queue, p); } @@ -1470,7 +1466,7 @@ static int limit_list(struct rev_info *revs) if (revs->max_age != -1 && (commit->date < revs->max_age)) obj->flags |= UNINTERESTING; - if (process_parents(revs, commit, NULL, &queue) < 0) { + if (process_parents(revs, commit, &queue) < 0) { clear_prio_queue(&queue); return -1; } @@ -3257,6 +3253,7 @@ static void free_void_commit_list(void *list) void release_revisions(struct rev_info *revs) { commit_list_free(revs->commits); + clear_prio_queue(&revs->commit_queue); commit_list_free(revs->ancestry_path_bottoms); release_display_notes(&revs->notes_opt); object_array_clear(&revs->pending); @@ -3726,7 +3723,7 @@ static void explore_walk_step(struct rev_info *revs) if (revs->max_age != -1 && (c->date < revs->max_age)) c->object.flags |= UNINTERESTING; - if (process_parents(revs, c, NULL, NULL) < 0) + if (process_parents(revs, c, NULL) < 0) return; if (c->object.flags & UNINTERESTING) @@ -3902,7 +3899,7 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit) { struct commit_list *p; struct topo_walk_info *info = revs->topo_walk_info; - if (process_parents(revs, commit, NULL, NULL) < 0) { + if (process_parents(revs, commit, NULL) < 0) { if (!revs->ignore_missing_links) die("Failed to traverse parents of commit %s", oid_to_hex(&commit->object.oid)); @@ -3938,6 +3935,13 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit) } } +void rev_info_commit_list_to_queue(struct rev_info *revs) +{ + while (revs->commits) + prio_queue_put(&revs->commit_queue, pop_commit(&revs->commits)); +} + + int prepare_revision_walk(struct rev_info *revs) { int i; @@ -4006,7 +4010,7 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs, for (;;) { struct commit *p = *pp; if (!revs->limited) - if (process_parents(revs, p, NULL, queue) < 0) + if (process_parents(revs, p, queue) < 0) return rewrite_one_error; if (p->object.flags & UNINTERESTING) return rewrite_one_ok; @@ -4020,27 +4024,18 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs, } } -static void merge_queue_into_list(struct prio_queue *q, struct commit_list **list) +static void merge_queue_into_prio_queue(struct prio_queue *from, + struct prio_queue *to) { - while (q->nr) { - struct commit *item = prio_queue_peek(q); - struct commit_list *p = *list; - - if (p && p->item->date >= item->date) - list = &p->next; - else { - p = commit_list_insert(item, list); - list = &p->next; /* skip newly added item */ - prio_queue_get(q); /* pop item */ - } - } + while (from->nr) + prio_queue_put(to, prio_queue_get(from)); } static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp) { struct prio_queue queue = { compare_commits_by_commit_date }; enum rewrite_result ret = rewrite_one_1(revs, pp, &queue); - merge_queue_into_list(&queue, &revs->commits); + merge_queue_into_prio_queue(&queue, &revs->commit_queue); clear_prio_queue(&queue); return ret; } @@ -4331,6 +4326,7 @@ enum rev_walk_mode { REV_WALK_REFLOG, REV_WALK_TOPO, REV_WALK_LIMITED, + REV_WALK_NO_WALK, REV_WALK_STREAMING, }; @@ -4342,6 +4338,8 @@ static enum rev_walk_mode get_walk_mode(struct rev_info *revs) return REV_WALK_TOPO; if (revs->limited) return REV_WALK_LIMITED; + if (revs->no_walk) + return REV_WALK_NO_WALK; return REV_WALK_STREAMING; } @@ -4349,6 +4347,9 @@ static struct commit *get_revision_1(struct rev_info *revs) { enum rev_walk_mode mode = get_walk_mode(revs); + if (mode == REV_WALK_STREAMING && revs->commits) + rev_info_commit_list_to_queue(revs); + while (1) { struct commit *commit; @@ -4360,9 +4361,12 @@ static struct commit *get_revision_1(struct rev_info *revs) commit = next_topo_commit(revs); break; case REV_WALK_LIMITED: - case REV_WALK_STREAMING: + case REV_WALK_NO_WALK: commit = pop_commit(&revs->commits); break; + case REV_WALK_STREAMING: + commit = prio_queue_get(&revs->commit_queue); + break; } if (!commit) @@ -4390,12 +4394,13 @@ static struct commit *get_revision_1(struct rev_info *revs) break; case REV_WALK_STREAMING: if (process_parents(revs, commit, - &revs->commits, NULL) < 0) { + &revs->commit_queue) < 0) { if (!revs->ignore_missing_links) die("Failed to traverse parents of commit %s", oid_to_hex(&commit->object.oid)); } break; + case REV_WALK_NO_WALK: case REV_WALK_LIMITED: break; } diff --git a/revision.h b/revision.h index 584f1338b5e323..04982a3d47f28f 100644 --- a/revision.h +++ b/revision.h @@ -12,6 +12,7 @@ #include "decorate.h" #include "ident.h" #include "list-objects-filter-options.h" +#include "prio-queue.h" #include "strvec.h" /** @@ -122,8 +123,14 @@ struct oidset; struct topo_walk_info; struct rev_info { - /* Starting list */ + /* + * Work queue of commits, stored as either a linked list or a + * priority queue, but never both at the same time. + * rev_info_commit_list_to_queue() converts list to queue. + */ struct commit_list *commits; + struct prio_queue commit_queue; + struct object_array pending; struct repository *repo; @@ -400,6 +407,7 @@ struct rev_info { * uninitialized. */ #define REV_INFO_INIT { \ + .commit_queue = { .compare = compare_commits_by_commit_date }, \ .abbrev = DEFAULT_ABBREV, \ .simplify_history = 1, \ .pruning.flags.recursive = 1, \ @@ -478,6 +486,8 @@ void reset_revision_walk(void); */ int prepare_revision_walk(struct rev_info *revs); +/* Drain the commits linked list into the priority queue. */ +void rev_info_commit_list_to_queue(struct rev_info *revs); /** * Takes a pointer to a `rev_info` structure and iterates over it, returning a * `struct commit *` each time you call it. The end of the revision list is From dc6068df67a5c4a36d4579ce783377a667411230 Mon Sep 17 00:00:00 2001 From: Weijie Yuan Date: Fri, 29 May 2026 16:17:04 +0800 Subject: [PATCH 04/21] docs: fix typos and grammar Fix several spelling mistakes, subject-verb agreement issues, and duplicated words. Signed-off-by: Weijie Yuan Signed-off-by: Junio C Hamano --- Documentation/fetch-options.adoc | 2 +- combine-diff.c | 2 +- contrib/subtree/t/t7900-subtree.sh | 2 +- csum-file.h | 2 +- delta-islands.c | 2 +- diffcore-pickaxe.c | 2 +- odb.h | 2 +- parse-options.c | 2 +- rerere.c | 2 +- t/t4203-mailmap.sh | 2 +- t/t9100-git-svn-basic.sh | 2 +- t/test-lib-github-workflow-markup.sh | 2 +- t/test-lib-junit.sh | 2 +- tree-walk.h | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index 81a9d7f9bbc11d..b3e387413fb6de 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -1,6 +1,6 @@ `--all`:: `--no-all`:: - Fetch all remotes, except for the ones that has the + Fetch all remotes, except for the ones that have the `remote..skipFetchAll` configuration variable set. This overrides the configuration variable `fetch.all`. diff --git a/combine-diff.c b/combine-diff.c index b7998620687ed7..720768ce41b5df 100644 --- a/combine-diff.c +++ b/combine-diff.c @@ -666,7 +666,7 @@ static int make_hunks(struct sline *sline, unsigned long cnt, * (-) line, which records from what parents the line * was removed; this line does not appear in the result. * then check the set of parents the result has difference - * from, from all lines. If there are lines that has + * from, from all lines. If there are lines that have * different set of parents that the result has differences * from, that means we have more than two versions. * diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh index 18d2b564487e91..4194687cfbb9b5 100755 --- a/contrib/subtree/t/t7900-subtree.sh +++ b/contrib/subtree/t/t7900-subtree.sh @@ -75,7 +75,7 @@ test_create_pre2_32_repo () { # # Create a simple subtree on a new branch named ORPHAN in REPO. # The subtree is then merged into the current branch of REPO, -# under PREFIX. The generated subtree has has one commit +# under PREFIX. The generated subtree has one commit # with subject and tag FILENAME with a single file "FILENAME.t" # # When this method returns: diff --git a/csum-file.h b/csum-file.h index a9b390d3366875..a270738a7a3cad 100644 --- a/csum-file.h +++ b/csum-file.h @@ -52,7 +52,7 @@ struct hashfd_options { */ struct progress *progress; - /* The length of the buffer that shall be used read read data. */ + /* The length of the buffer that shall be used to read data. */ size_t buffer_len; }; diff --git a/delta-islands.c b/delta-islands.c index f4d2468790ce4f..e71a7e1c055dc8 100644 --- a/delta-islands.c +++ b/delta-islands.c @@ -527,7 +527,7 @@ void free_island_marks(void) kh_destroy_oid_map(island_marks); } - /* detect use-after-free with a an address which is never valid: */ + /* detect use-after-free with an address which is never valid: */ island_marks = (void *)-1; } diff --git a/diffcore-pickaxe.c b/diffcore-pickaxe.c index a52d569911c48e..b0915be86fc475 100644 --- a/diffcore-pickaxe.c +++ b/diffcore-pickaxe.c @@ -203,7 +203,7 @@ static void pickaxe(struct diff_queue_struct *q, struct diff_options *o, for (i = 0; i < q->nr; i++) diff_free_filepair(q->queue[i]); } else { - /* Showing only the filepairs that has the needle */ + /* Showing only the filepairs that have the needle */ for (i = 0; i < q->nr; i++) { struct diff_filepair *p = q->queue[i]; if (pickaxe_match(p, o, regexp, kws, fn)) diff --git a/odb.h b/odb.h index 3a711f6547bb00..24b84511931376 100644 --- a/odb.h +++ b/odb.h @@ -57,7 +57,7 @@ struct object_database { struct repository *repo; /* - * State of current current object database transaction. Only one + * State of current object database transaction. Only one * transaction may be pending at a time. Is NULL when no transaction is * configured. */ diff --git a/parse-options.c b/parse-options.c index a676da86f5d617..f4647e0099ea99 100644 --- a/parse-options.c +++ b/parse-options.c @@ -1149,7 +1149,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, (ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT)) { /* * Found an unknown option given to a command with - * subcommands that has a default operation mode: + * subcommands that have a default operation mode: * we treat this option and all remaining args as * arguments meant to that default operation mode. * So we are done parsing. diff --git a/rerere.c b/rerere.c index 0296700f9f448f..28a740b771cf99 100644 --- a/rerere.c +++ b/rerere.c @@ -548,7 +548,7 @@ static int check_one_conflict(struct index_state *istate, int i, int *type) /* * Scan the index and find paths that have conflicts that rerere can - * handle, i.e. the ones that has both stages #2 and #3. + * handle, i.e. the ones that have both stages #2 and #3. * * NEEDSWORK: we do not record or replay a previous "resolve by * deletion" for a delete-modify conflict, as that is inherently risky diff --git a/t/t4203-mailmap.sh b/t/t4203-mailmap.sh index 74b7ddccb26d59..03f6df9d244890 100755 --- a/t/t4203-mailmap.sh +++ b/t/t4203-mailmap.sh @@ -180,7 +180,7 @@ test_expect_success 'mailmap.file set' ' git shortlog HEAD >actual && test_cmp expect actual && - # The internal_mailmap/.mailmap file is an a subdirectory, but + # The internal_mailmap/.mailmap file is in a subdirectory, but # as shown here it can also be outside the repository test_when_finished "rm -rf sub-repo" && git clone . sub-repo && diff --git a/t/t9100-git-svn-basic.sh b/t/t9100-git-svn-basic.sh index af28b01fefa49c..1ab98b9c373a7e 100755 --- a/t/t9100-git-svn-basic.sh +++ b/t/t9100-git-svn-basic.sh @@ -232,7 +232,7 @@ test_expect_success POSIXPERM,SYMLINKS "$name" ' test_cmp expected.$(test_oid algo) a ' -test_expect_success 'exit if remote refs are ambigious' ' +test_expect_success 'exit if remote refs are ambiguous' ' git config --add svn-remote.svn.fetch \ bar:refs/remotes/git-svn && test_must_fail git svn migrate diff --git a/t/test-lib-github-workflow-markup.sh b/t/test-lib-github-workflow-markup.sh index 33405c90d740d4..fa29a62aa311c5 100644 --- a/t/test-lib-github-workflow-markup.sh +++ b/t/test-lib-github-workflow-markup.sh @@ -18,7 +18,7 @@ # # The idea is for `test-lib.sh` to source this file when run in GitHub # workflows; these functions will then override (empty) functions -# that are are called at the appropriate times during the test runs. +# that are called at the appropriate times during the test runs. test_skip_test_preamble=t diff --git a/t/test-lib-junit.sh b/t/test-lib-junit.sh index 76cbbd3299d64a..f4994dd9d3183d 100644 --- a/t/test-lib-junit.sh +++ b/t/test-lib-junit.sh @@ -19,7 +19,7 @@ # # The idea is for `test-lib.sh` to source this file when the user asks # for JUnit XML; these functions will then override (empty) functions -# that are are called at the appropriate times during the test runs. +# that are called at the appropriate times during the test runs. start_test_output () { junit_xml_dir="$TEST_OUTPUT_DIRECTORY/out" diff --git a/tree-walk.h b/tree-walk.h index 29a55328bd94a4..9646c47ac5bce5 100644 --- a/tree-walk.h +++ b/tree-walk.h @@ -177,7 +177,7 @@ struct traverse_info { /** * Walk trees starting with "tree_oid" to find the entry for "name", and - * return the the object name and the mode of the found entry via the + * return the object name and the mode of the found entry via the * "oid" and "mode" parameters. Return 0 if the entry is found, and -1 * otherwise. */ From 061a68e4433b98ca351dcbf887b890aa2ec1bdb8 Mon Sep 17 00:00:00 2001 From: Michael Montalbo Date: Mon, 1 Jun 2026 21:20:47 +0000 Subject: [PATCH 05/21] sub-process: use gentle handshake to avoid die() on startup failure When the configured subprocess command contains shell metacharacters (such as a space), prepare_shell_cmd() wraps it in "sh -c ". The shell itself always starts successfully, so start_command() returns zero even if the tool inside does not exist. The subsequent handshake then reads from a dead pipe and calls die() via the non-gentle packet_read_line(), killing the parent process instead of letting it handle the error. Before this change, a missing filter process at a path containing spaces produces a confusing error: $ git -c filter.myfilter.process="/path with space/tool" \ -c filter.myfilter.required=true add file.txt /path with space/tool: line 1: /path: No such file or directory fatal: the remote end hung up unexpectedly After this change, the proper error is reported: $ git ... add file.txt /path with space/tool: line 1: /path: No such file or directory error: could not read greeting from subprocess '/path with space/tool' error: initialization for subprocess '/path with space/tool' failed fatal: file.txt: clean filter 'myfilter' failed Switch the subprocess handshake from the dying packet_read_line() to packet_read_line_gently() so that a process that exits during startup produces an error return instead of killing the caller. This affects any subprocess consumer whose command path contains spaces. On Windows this routinely happens because programs live under "C:/Program Files/...", and MSYS2 path conversion can rewrite absolute paths to include that prefix. On POSIX it triggers whenever the configured path naturally contains a space or other metacharacter. convert.c (filter..process, used by git-lfs and custom clean/smudge filters) is the primary affected consumer. Signed-off-by: Michael Montalbo Signed-off-by: Junio C Hamano --- sub-process.c | 26 ++++++++++++++++++++------ t/t0021-conversion.sh | 17 +++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/sub-process.c b/sub-process.c index 83bf0a0e82e56d..2d5c965169727b 100644 --- a/sub-process.c +++ b/sub-process.c @@ -132,17 +132,24 @@ static int handshake_version(struct child_process *process, if (packet_flush_gently(process->in)) return error("Could not write flush packet"); - if (!(line = packet_read_line(process->out, NULL)) || - !skip_prefix(line, welcome_prefix, &p) || + if (packet_read_line_gently(process->out, NULL, &line) < 0) + return error("could not read greeting from subprocess '%s'", + process->args.v[0]); + if (!line || !skip_prefix(line, welcome_prefix, &p) || strcmp(p, "-server")) return error("Unexpected line '%s', expected %s-server", line ? line : "", welcome_prefix); - if (!(line = packet_read_line(process->out, NULL)) || - !skip_prefix(line, "version=", &p) || + if (packet_read_line_gently(process->out, NULL, &line) < 0) + return error("could not read version from subprocess '%s'", + process->args.v[0]); + if (!line || !skip_prefix(line, "version=", &p) || strtol_i(p, 10, chosen_version)) return error("Unexpected line '%s', expected version", line ? line : ""); - if ((line = packet_read_line(process->out, NULL))) + if (packet_read_line_gently(process->out, NULL, &line) < 0) + return error("could not read version flush from subprocess '%s'", + process->args.v[0]); + if (line) return error("Unexpected line '%s', expected flush", line); /* Check to make sure that the version received is supported */ @@ -171,8 +178,15 @@ static int handshake_capabilities(struct child_process *process, if (packet_flush_gently(process->in)) return error("Could not write flush packet"); - while ((line = packet_read_line(process->out, NULL))) { + for (;;) { const char *p; + int len = packet_read_line_gently(process->out, NULL, &line); + + if (len < 0) + return error("could not read capabilities from subprocess '%s'", + process->args.v[0]); + if (!line) + break; if (!skip_prefix(line, "capability=", &p)) continue; diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh index f0d50d769e9fc5..033b00a364ee7a 100755 --- a/t/t0021-conversion.sh +++ b/t/t0021-conversion.sh @@ -857,6 +857,23 @@ test_expect_success 'invalid process filter must fail (and not hang!)' ' ) ' +test_expect_success 'missing process filter with space in path does not die' ' + test_config_global filter.protocol.process "/non existent/tool" && + test_config_global filter.protocol.required true && + rm -rf repo && + mkdir repo && + ( + cd repo && + git init && + + echo "*.r filter=protocol" >.gitattributes && + + cp "$TEST_ROOT/test.o" test.r && + test_must_fail git add . 2>git-stderr.log && + test_grep "clean filter.*protocol.*failed" git-stderr.log + ) +' + test_expect_success 'delayed checkout in process filter' ' test_config_global filter.a.process "test-tool rot13-filter --log=a.log clean smudge delay" && test_config_global filter.a.required true && From 1891707d1b8bb0ac3c47343e881fcf28ec69457a Mon Sep 17 00:00:00 2001 From: Jacob Keller Date: Mon, 1 Jun 2026 16:36:08 -0700 Subject: [PATCH 06/21] describe: fix --exclude, --match with --contains and --all git describe --contains acts as a wrapper around git name-rev. When operating with --contains and --all, the --match and --exclude patterns are not properly forwarded to name-rev as --exclude and --refs options. This results in the command silently discarding match and exclude requests from the user when operating in --all mode. We could check and die() if the user provides --contains, --all, and --match/--exclude. However, its also straight forward to just pass the filters down to git name-rev. Notice that the documentation for --match and --exclude mention the --all mode. It explains that they operate on refs with the prefix refs/tags, and additionally refs/heads and refs/remotes when using --all. Fix the describe logic to pass the patterns down with the appropriate prefixes when --all is provided. This fixes the support to match the documented behavior. Add tests to check that this works as expected. Reported-by: Tuomas Ahola Signed-off-by: Jacob Keller Signed-off-by: Junio C Hamano --- builtin/describe.c | 18 +++++++++++++++--- t/t6120-describe.sh | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/builtin/describe.c b/builtin/describe.c index bffeed13a3cb14..62800ef15ed915 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -712,13 +712,25 @@ int cmd_describe(int argc, NULL); if (always) strvec_push(&args, "--always"); - if (!all) { + if (!all) strvec_push(&args, "--tags"); + + for_each_string_list_item(item, &patterns) + strvec_pushf(&args, "--refs=refs/tags/%s", item->string); + for_each_string_list_item(item, &exclude_patterns) + strvec_pushf(&args, "--exclude=refs/tags/%s", item->string); + + if (all) { for_each_string_list_item(item, &patterns) - strvec_pushf(&args, "--refs=refs/tags/%s", item->string); + strvec_pushf(&args, "--refs=refs/heads/%s", item->string); for_each_string_list_item(item, &exclude_patterns) - strvec_pushf(&args, "--exclude=refs/tags/%s", item->string); + strvec_pushf(&args, "--exclude=refs/heads/%s", item->string); + for_each_string_list_item(item, &patterns) + strvec_pushf(&args, "--refs=refs/remotes/%s", item->string); + for_each_string_list_item(item, &exclude_patterns) + strvec_pushf(&args, "--exclude=refs/remotes/%s", item->string); } + if (argc) strvec_pushv(&args, argv); else diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index 2c70cc561ad5f6..e5bcf537602a21 100755 --- a/t/t6120-describe.sh +++ b/t/t6120-describe.sh @@ -345,6 +345,28 @@ test_expect_success 'describe --contains and --no-match' ' test_cmp expect actual ' +test_expect_success 'describe --contains --all --match no matching commit' ' + echo "tags/A^0" >expect && + tagged_commit=$(git rev-parse "refs/tags/A^0") && + test_must_fail git describe --contains --all --match="B" $tagged_commit +' + +check_describe "tags/A^0" --contains --all --match="A" $(git rev-parse "refs/tags/A^0") + +check_describe "branch_A" --contains --all --match="branch*" $(git rev-parse "refs/tags/A^0") + +check_describe "branch_C~1" --contains --all --match="branch*" --exclude="branch_A" $(git rev-parse "refs/tags/A^0") + +check_describe "branch_A" --contains --all \ + --exclude="A" --exclude="c" --exclude="test*" --exclude="origin/remote_branch_A" \ + $(git rev-parse "refs/tags/A^0") + +check_describe "remotes/origin/remote_branch_A" --contains --all --match="origin/remote*" $(git rev-parse "refs/tags/A^0") + +check_describe "remotes/origin/remote_branch_C~1" --contains --all \ + --match="origin/remote*" --exclude="origin/remote_branch_A" \ + $(git rev-parse "refs/tags/A^0") + test_expect_success 'setup and absorb a submodule' ' test_create_repo sub1 && test_commit -C sub1 initial && From bb4ce23284d3605c892fdf4fe349fe8773c813d2 Mon Sep 17 00:00:00 2001 From: Mirko Faina Date: Tue, 19 May 2026 02:55:22 +0200 Subject: [PATCH 07/21] revision.c: implement --max-count-oldest "--max-count" is a commit limiting option and sets a maximum amount of commits to be shown. If a user wants to see only the first N commits of the history (the oldest commits) they'd have to do something like git log $(git rev-list HEAD | tail -n N | head -n 1) This is not very user-friendly. Teach get_revision() the --max-count-oldest option. Signed-off-by: Mirko Faina [jc: fixed up t4202 ] Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.adoc | 5 +- revision.c | 111 +++++++++++++++++++++++++++- revision.h | 2 + t/t4202-log.sh | 40 ++++++++++ 4 files changed, 154 insertions(+), 4 deletions(-) diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc index 2d195a147456ea..e8c88d0f1c758f 100644 --- a/Documentation/rev-list-options.adoc +++ b/Documentation/rev-list-options.adoc @@ -16,7 +16,10 @@ ordering and formatting options, such as `--reverse`. `-`:: `-n `:: `--max-count=`:: - Limit the output to __ commits. + Limit the output to the first __ commits that would be shown. + +`--max-count-oldest=`:: + Limit the output to the last __ commits that would be shown. `--skip=`:: Skip __ commits before starting to show the commit output. diff --git a/revision.c b/revision.c index 599b3a66c369ca..5d53db3152ddf0 100644 --- a/revision.c +++ b/revision.c @@ -2339,10 +2339,28 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg } if ((argcount = parse_long_opt("max-count", argv, &optarg))) { + if (revs->max_count_type == 1) + die_for_incompatible_opt2(1, "--max-count", 1, + "--max-count-oldest"); revs->max_count = parse_count(optarg); revs->no_walk = 0; + revs->max_count_type = 0; return argcount; + } else if ((argcount = parse_long_opt("max-count-oldest", argv, &optarg))) { + if (revs->max_count_type == 0 && revs->max_count != -1) + die_for_incompatible_opt2(1, "--max-count", 1, + "--max-count-oldest"); + if (revs->skip_count > 0) + die_for_incompatible_opt2(1, "--skip", 1, + "--max-count-oldest"); + revs->max_count = parse_count(optarg); + revs->no_walk = 0; + revs->max_count_type = 1; + revs->max_count_stage = 0; } else if ((argcount = parse_long_opt("skip", argv, &optarg))) { + if (revs->max_count_type == 1) + die_for_incompatible_opt2(1, "--skip", 1, + "--max-count-oldest"); revs->skip_count = parse_count(optarg); return argcount; } else if ((*arg == '-') && isdigit(arg[1])) { @@ -4521,15 +4539,91 @@ static struct commit *get_revision_internal(struct rev_info *revs) return c; } +static void retrieve_oldest_commits(struct rev_info *revs, + struct commit_list **queue) +{ + struct commit *c; + int max_count = revs->max_count; + int queuei_count = 0; + int queueo_count = 0; + struct commit_list *queueo = NULL; + struct commit_list *queuei = NULL; + struct commit_list *reversed_queue = NULL; + struct commit_list *p; + + revs->max_count = -1; + while ((c = get_revision_internal(revs))) { + /* + * We need to reset SHOWN status otherwise --graph breaks. + * It is fine to do, get_revision_internal() doesn't consider + * children commits as they have been already processed and the + * traversal happens only child to parent. + * + * We do this because the --graph machinery relies on the status + * of the parents to decide how the printing will happen. + * + * We can't simply replace this instruction with a + * graph_update() as it doesn't do the actualy printing, we'd + * have to remove any commit that goes over the + * --max-count-oldest limit from revs->graph. + */ + c->object.flags &= ~(SHOWN | CHILD_SHOWN); + commit_list_insert(c, &queuei); + if (!(c->object.flags & BOUNDARY)) + queuei_count++; + while (queuei_count + queueo_count > max_count) { + if (!queueo_count) { + while ((c = pop_commit(&queuei))) { + commit_list_insert(c, &queueo); + queueo_count++; + } + queuei_count = 0; + } + c = pop_commit(&queueo); + queueo_count--; + /* We need to do this otherwise we'll discard the + * commits that go over the --max-count-oldest limit but + * not their respective boundaries. This matters only if + * we're discarding the commit right before the boundary. + */ + for (p = c->parents; p; p = p->next) + p->item->object.flags &= ~CHILD_SHOWN; + } + } + + while ((c = pop_commit(&queueo))) + commit_list_insert(c, &reversed_queue); + while ((c = pop_commit(&queuei))) + commit_list_insert(c, &queueo); + while ((c = pop_commit(&queueo))) + commit_list_insert(c, &reversed_queue); + + while ((c = pop_commit(&reversed_queue))) + commit_list_insert(c, queue); +} + struct commit *get_revision(struct rev_info *revs) { struct commit *c; struct commit_list *reversed; + struct commit_list *queue = NULL; + struct commit_list *p; + + if (revs->max_count_type == 1 && !revs->max_count_stage) { + retrieve_oldest_commits(revs, &queue); + commit_list_free(revs->commits); + revs->commits = queue; + revs->max_count_stage = 1; + } if (revs->reverse) { reversed = NULL; - while ((c = get_revision_internal(revs))) - commit_list_insert(c, &reversed); + if (revs->max_count_type == 1) + while ((c = pop_commit(&revs->commits))) + commit_list_insert(c, &reversed); + else + while ((c = get_revision_internal(revs))) + commit_list_insert(c, &reversed); commit_list_free(revs->commits); revs->commits = reversed; revs->reverse = 0; @@ -4543,7 +4637,18 @@ struct commit *get_revision(struct rev_info *revs) return c; } - c = get_revision_internal(revs); + if (revs->max_count_stage) { + c = pop_commit(&revs->commits); + if (c) { + c->object.flags |= SHOWN; + if (!(c->object.flags & BOUNDARY)) + for (p = c->parents; p; p = p->next) + p->item->object.flags |= CHILD_SHOWN; + } + } else { + c = get_revision_internal(revs); + } + if (c && revs->graph) graph_update(revs->graph, c); if (!c) { diff --git a/revision.h b/revision.h index 584f1338b5e323..e157463cb1f62c 100644 --- a/revision.h +++ b/revision.h @@ -309,6 +309,8 @@ struct rev_info { /* special limits */ int skip_count; int max_count; + unsigned int max_count_type:1; + unsigned int max_count_stage:1; timestamp_t max_age; timestamp_t max_age_as_filter; timestamp_t min_age; diff --git a/t/t4202-log.sh b/t/t4202-log.sh index 05cee9e41bb48d..75edb0eb38c039 100755 --- a/t/t4202-log.sh +++ b/t/t4202-log.sh @@ -1882,6 +1882,46 @@ test_expect_success 'log --graph with --name-status' ' test_cmp_graph --name-status tangle..reach ' +test_expect_success 'log --max-count-oldest=3 --oneline' ' + test_when_finished rm expect && + git log --oneline | tail -n3 >expect && + git log --oneline --max-count-oldest=3 >actual && + test_cmp expect actual +' + +test_expect_success 'log --max-count-oldest=3 --reverse --oneline' ' + test_when_finished rm expect && + git log --oneline --reverse | head -n3 >expect && + git log --oneline --max-count-oldest=3 --reverse >actual && + test_cmp expect actual +' + +test_expect_success 'log --max-count-oldest with --max-count' ' + test_when_finished rm stderr && + test_must_fail git log --max-count-oldest=3 --max-count=3 2>stderr && + test_grep "cannot be used together" stderr +' + +test_expect_success 'log --max-count-oldest with --skip' ' + test_when_finished rm stderr && + test_must_fail git log --max-count-oldest=3 --skip=1 2>stderr && + test_grep "cannot be used together" stderr +' + +test_expect_success 'log --max-count-oldest=1000 --graph --boundary' ' + test_when_finished rm expect actual && + git log --graph --boundary >expect && + git log --max-count-oldest=1000 --graph --boundary >actual && + test_cmp expect actual +' + +test_expect_success 'log --oneline --graph --boundary --max-count-oldest=1' ' + test_when_finished rm -f actual && + git log --oneline --graph --boundary --max-count-oldest=1 \ + HEAD~1..HEAD >actual && + test_line_count = 2 actual +' + cat >expect <<-\EOF * reach | From 9708b3dc95a22116c4a058b107b063da4bcf7d4a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:31 +0200 Subject: [PATCH 08/21] gitlab-ci: rearrange Linux jobs to match GitHub's order Rearrange the order of Linux jobs that we have defined in GitLab CI so that it matches the order on GitHub's side. This makes it easier to compare whether the list of jobs actually matches on both sides. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- .gitlab-ci.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 83ec786c5a49d0..c4eec6e7651300 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,15 +42,15 @@ test:linux: - jobname: linux-reftable image: ubuntu:rolling CC: clang + - jobname: linux-TEST-vars + image: ubuntu:20.04 + CC: gcc + CC_PACKAGE: gcc-8 - jobname: linux-breaking-changes image: ubuntu:20.04 CC: gcc - jobname: fedora-breaking-changes-meson image: fedora:latest - - jobname: linux-TEST-vars - image: ubuntu:20.04 - CC: gcc - CC_PACKAGE: gcc-8 - jobname: linux-leaks image: ubuntu:rolling CC: gcc @@ -60,13 +60,14 @@ test:linux: - jobname: linux-asan-ubsan image: ubuntu:rolling CC: clang + - jobname: linux-meson + image: ubuntu:rolling + CC: gcc - jobname: linux-musl-meson image: alpine:latest + # Supported until 2025-04-02. - jobname: linux32 image: i386/ubuntu:20.04 - - jobname: linux-meson - image: ubuntu:rolling - CC: gcc artifacts: paths: - t/failed-test-artifacts From f0ba41bae89ae5bb66d1b9677d26bd6d7953da34 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:32 +0200 Subject: [PATCH 09/21] gitlab-ci: add missing Linux jobs The GitLab CI definitions are missing jobs for AlmaLinux and Debian, both of which exist in GitHub Workflows. Plug this gap. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- .gitlab-ci.yml | 6 ++++++ ci/lib.sh | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c4eec6e7651300..2b9ed44eaf24c4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,6 +68,12 @@ test:linux: # Supported until 2025-04-02. - jobname: linux32 image: i386/ubuntu:20.04 + # A RHEL 8 compatible distro. Supported until 2029-05-31. + - jobname: almalinux-8 + image: almalinux:8 + # Supported until 2026-08-31. + - jobname: debian-11 + image: debian:11 artifacts: paths: - t/failed-test-artifacts diff --git a/ci/lib.sh b/ci/lib.sh index 6e3799cfc3ccd5..b939110a6eefcf 100755 --- a/ci/lib.sh +++ b/ci/lib.sh @@ -254,7 +254,7 @@ then CI_OS_NAME=osx JOBS=$(nproc) ;; - *,alpine:*|*,fedora:*|*,ubuntu:*|*,i386/ubuntu:*) + *,almalinux:*|*,alpine:*|*,debian:*|*,fedora:*|*,ubuntu:*|*,i386/ubuntu:*) CI_OS_NAME=linux JOBS=$(nproc) ;; From 43a6a005c8970f13c46202e821111f9e42538b9d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:33 +0200 Subject: [PATCH 10/21] ci: unify Linux images across GitLab and GitHub The image for the "linux-breaking-changes" job has drifted apart across GitHub and GitLab. Adapt it to use "ubuntu:rolling" on both systems. With this change there's only one difference remaining: GitHub uses "ubuntu:focal" for the "linux32" job while GitLab uses "ubuntu:20.04". These are different names for the same image, so there is no actual difference here. Adjust GitHub to use the "20.04" tag -- this matches all the other jobs which use version numbers, and you don't have to learn Ubuntu's release names by heart. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- .github/workflows/main.yml | 2 +- .gitlab-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3da5326f0ba90a..cf341d74dbff21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -407,7 +407,7 @@ jobs: image: alpine:latest # Supported until 2025-04-02. - jobname: linux32 - image: i386/ubuntu:focal + image: i386/ubuntu:20.04 # A RHEL 8 compatible distro. Supported until 2029-05-31. - jobname: almalinux-8 image: almalinux:8 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b9ed44eaf24c4..ef1c723355d153 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,7 +47,7 @@ test:linux: CC: gcc CC_PACKAGE: gcc-8 - jobname: linux-breaking-changes - image: ubuntu:20.04 + image: ubuntu:rolling CC: gcc - jobname: fedora-breaking-changes-meson image: fedora:latest From bf3ed750cb4479db9e8193ae1937b2723054ce48 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:34 +0200 Subject: [PATCH 11/21] t7527: fix broken TAP output Before running the tests in t7527 we first verify whether the fsmonitor even works, which seems to depend on the actual filesystem that is in use. The verification executes outside of any prerequisite or test body, so its stdout/stderr is not being redirected. The consequence of this is that any command that prints to stdout/stderr may break the TAP specification by printing invalid lines. And in fact we already do that, as git-init(1) prints the path to the created Git repository by default. Fix this issue by moving the logic into a lazy prerequisite. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t7527-builtin-fsmonitor.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh index b63c162f9bac3f..d881e27466c18e 100755 --- a/t/t7527-builtin-fsmonitor.sh +++ b/t/t7527-builtin-fsmonitor.sh @@ -25,7 +25,8 @@ maybe_timeout () { "$@" fi } -verify_fsmonitor_works () { + +test_lazy_prereq FSMONITOR_WORKS ' git init test_fsmonitor_smoke || return 1 GIT_TRACE_FSMONITOR="$PWD/smoke.trace" && @@ -50,9 +51,9 @@ verify_fsmonitor_works () { ret=$? rm -rf test_fsmonitor_smoke smoke.trace return $ret -} +' -if ! verify_fsmonitor_works +if ! test_have_prereq FSMONITOR_WORKS then skip_all="filesystem does not deliver fsmonitor events (container/overlayfs?)" test_done From b1688db759de18a8403945090688b8cc25ba26dd Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:35 +0200 Subject: [PATCH 12/21] t7810: turn MB_REGEX check into a lazy prereq In t7810 we verify whether the system has proper multibyte locale support by executing `test-tool regex` with a unicode character. When this check fails though we'll output an error that breaks the TAP format. Fix this issue by turning the logic into a lazy prerequisite. Reported-by: Jeff King Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t7810-grep.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh index 1b195bee599a37..d61c4a4d73c390 100755 --- a/t/t7810-grep.sh +++ b/t/t7810-grep.sh @@ -18,8 +18,9 @@ test_invalid_grep_expression() { ' } -LC_ALL=en_US.UTF-8 test-tool regex '^.$' '¿' && - test_set_prereq MB_REGEX +test_lazy_prereq MB_REGEX ' + LC_ALL=en_US.UTF-8 test-tool regex "^.$" "¿" +' cat >hello.c < From d11968661e641ea81f4c1938ae9f73a54107dc62 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:36 +0200 Subject: [PATCH 13/21] t/test-lib: silence EBUSY errors on Windows during test cleanup When tests have finished we clean up the trash directory via `rm -rf`. On Windows this can fail with EBUSY in cases where a process still holds some of the files open, for example when we have spawned a daemonized process that wasn't properly terminated. We thus retry several times, but every failure will result in error messages being printed, and that in turn breaks the TAP output format. One such case where this is causing issues is in t921x, which contains tests related to Scalar. Some tests spawn the fsmonitor daemon, and we never properly terminate it. The obvious fix would be to ensure that we never leak any processes, but that gets ugly fast. Instead, let's work around the issue by silencing error messages printed by the `rm -rf` calls. We already know to print an error when the retry loop fails, so we don't loose much. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/test-lib.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/test-lib.sh b/t/test-lib.sh index 4a7357b547e77e..d1d24c4124fd1d 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1299,10 +1299,10 @@ test_done () { error "Tests passed but trash directory already removed before test cleanup; aborting" cd "$TRASH_DIRECTORY/.." && - rm -fr "$TRASH_DIRECTORY" || { + rm -fr "$TRASH_DIRECTORY" 2>/dev/null || { # try again in a bit sleep 5; - rm -fr "$TRASH_DIRECTORY" + rm -fr "$TRASH_DIRECTORY" 2>/dev/null } || error "Tests passed but test cleanup failed; aborting" fi From c2d2d173ae6ba4b354a36b3ba732c8a11379d6ec Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:37 +0200 Subject: [PATCH 14/21] t/lib-git-p4: silence output when killing p4d and its watchdog When stopping the p4d watchdog process via "kill -9", the shell may print a job-control notification like: ./test-lib.sh: line 1269: 57960 Killed: 9 while true; do if test $nr_tries_left -eq 0; then kill -9 $p4d_pid; exit 1; fi; sleep 1; nr_tries_left=$(($nr_tries_left - 1)); done 2> /dev/null 4>&2 (wd: ~) This message is printed asynchronously by the shell when it reaps the process. While harmless right now, this will cause breakage once we enable strict parsing of the TAP protocol in a subsequent commit. Fix this by using `wait` so that we can synchronously reap the watchdog process and swallow the diagnostic. While at it, deduplicate the logic we have in `stop_p4d_and_watchdog ()` and `stop_and_cleanup_p4d ()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/lib-git-p4.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/lib-git-p4.sh b/t/lib-git-p4.sh index d22e9c684a495a..910886818768f0 100644 --- a/t/lib-git-p4.sh +++ b/t/lib-git-p4.sh @@ -65,6 +65,7 @@ pidfile="$TRASH_DIRECTORY/p4d.pid" stop_p4d_and_watchdog () { kill -9 $p4d_pid $watchdog_pid + wait $p4d_pid $watchdog_pid 2>/dev/null } # git p4 submit generates a temp file, which will @@ -174,8 +175,7 @@ retry_until_success () { } stop_and_cleanup_p4d () { - kill -9 $p4d_pid $watchdog_pid - wait $p4d_pid + stop_p4d_and_watchdog rm -rf "$db" "$cli" "$pidfile" } From 389c83025dbde15d30d0791281133bf30e45078d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:38 +0200 Subject: [PATCH 15/21] t: let prove fail when parsing invalid TAP output To make the result of our tests accessible we use the TAP protocol. This protocol is parsed by either prove or by Meson. Unfortunately, these two tools differ when it comes to their strictness when parsing the protocol: - Prove by default happily accepts lines not specified by the protocol. - Meson will also accept such lines, but prints a big and ugly warning message. We have fixed our test suite in the past to not print invalid TAP lines anymore via b1dc2e796e (Merge branch 'ps/meson-tap-parse', 2025-06-17). But as none of our tools perform a strict check it's still possible for broken tests to sneak back in, like for example in 362f69547f (Merge branch 'ps/t1006-tap-fix', 2025-07-16). This doesn't hurt at all when using prove, but it's quite annoying when using Meson due to the generated warnings. Unfortunately, there doesn't seem to be a portable way to make all tools complain about violations of the TAP format. The TAP 14 specification has added pragmas to the protocol that would allow us to say `pragma +strict`, and the effect of that would be to treat invalid TAP lines as a test failure. But the release of TAP 14 is still rather recent, and Test-Harness for example only gained support for it in version 3.48, which was released in 2023. In fact though, this pragma was already introduced as an inofficial extension of the TAP protocol with Test-Harness 3.10, released in 2008. So while not all tools understand the pragma, at least prove does for a long time. Unconditionally enable the pragma when using prove so that we'll detect tests that emit broken TAP output right away. This would have detected the issues fixed in preceding commits: $ prove t7527-builtin-fsmonitor.sh t7527-builtin-fsmonitor.sh .. All 69 subtests passed (less 6 skipped subtests: 63 okay) Test Summary Report ------------------- t7527-builtin-fsmonitor.sh (Wstat: 0 Tests: 69 Failed: 0) Parse errors: Unknown TAP token: "Initialized empty Git repository in /tmp/git/test_fsmonitor_smoke/.git/" Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/test-lib.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/t/test-lib.sh b/t/test-lib.sh index d1d24c4124fd1d..ceefb99bff60e0 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1532,6 +1532,12 @@ then BAIL_OUT 'You need to build test-tool; Run "make t/helper/test-tool" in the source (toplevel) directory' fi +if test -n "$HARNESS_ACTIVE" +then + say "TAP version 13" + say "pragma +strict" +fi + # Are we running this test at all? remove_trash= this_test=${0##*/} From e6145d12413044f90ee31eb7d83ae209aeb0adff Mon Sep 17 00:00:00 2001 From: Tuomas Ahola Date: Thu, 4 Jun 2026 16:14:57 +0300 Subject: [PATCH 16/21] docs: fix typos Fix some typos and grammar errors in comments and documentation files. Signed-off-by: Tuomas Ahola Signed-off-by: Junio C Hamano --- Documentation/config/sideband.adoc | 2 +- Documentation/git-format-rev.adoc | 2 +- date.c | 2 +- replay.h | 2 +- t/t9902-completion.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Documentation/config/sideband.adoc b/Documentation/config/sideband.adoc index 96fade7f5fee39..ff007aeb738a3d 100644 --- a/Documentation/config/sideband.adoc +++ b/Documentation/config/sideband.adoc @@ -13,7 +13,7 @@ sideband.allowControlCharacters:: Allow control sequences that move the cursor. This is disabled by default. `erase`:: - Allow control sequences that erase charactrs. This is + Allow control sequences that erase characters. This is disabled by default. `false`:: Mask all control characters other than line feeds and diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc index c40d52e9f6d108..505a52feccd466 100644 --- a/Documentation/git-format-rev.adoc +++ b/Documentation/git-format-rev.adoc @@ -33,7 +33,7 @@ OPTIONS The argument `rev` is also accepted. `text`;; Formats all commit object names found in freeform text. These - must the full object names, i.e. abbreviated hexidecimal object + must be full object names, i.e. abbreviated hexadecimal object names will not be interpreted. + Anything that is parsed as an object name but that is not found to be a diff --git a/date.c b/date.c index 05b78d852f0705..014065b419aee7 100644 --- a/date.c +++ b/date.c @@ -1074,7 +1074,7 @@ void datestamp(struct strbuf *out) * * The tm->tm_mday field has an additional logic of using negative values * for date adjustments: -2 means yesterday and -3 the day before that, - * and so on. The idea is to deref such adjustments until we are sure + * and so on. The idea is to defer such adjustments until we are sure * there's no explicit mday specification in the approxidate string. */ static time_t update_tm(struct tm *tm, struct tm *now, time_t sec) diff --git a/replay.h b/replay.h index 1851a07705ab03..faf95c7459e594 100644 --- a/replay.h +++ b/replay.h @@ -32,7 +32,7 @@ struct replay_revisions_options { /* * Starting point at which to create the new commits; must be a - * committish. References pointing at decendants of `onto` will be + * committish. References pointing at descendants of `onto` will be * updated to point to the new commits. */ const char *onto; diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 28f61f08fb4cec..55dc9eabfc42fe 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2444,7 +2444,7 @@ test_expect_success FUNNYNAMES \ >repeated-quoted/2-file && >repeated-quoted/3\"file && # ... and here, too. - # Still, we shold only list the directory name only once. + # Still, we should list the directory name only once. test_path_completion repeated repeated-quoted ' From 179f122aea7074a21ea095337c96801695ba3729 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 4 Jun 2026 16:24:19 +0000 Subject: [PATCH 17/21] mingw: kill child processes in a gentler way The TerminateProcess() function does not actually leave the child processes any chance to perform any cleanup operations. This is bad insofar as Git itself expects its signal handlers to run. A symptom is e.g. a left-behind .lock file that would not be left behind if the same operation was run, say, on Linux. To remedy this situation, we use an obscure trick: we inject a thread into the process that needs to be killed and to let that thread run the ExitProcess() function with the desired exit status. Thanks J Wyman for describing this trick. The advantage is that the ExitProcess() function lets the atexit handlers run. While this is still different from what Git expects (i.e. running a signal handler), in practice Git sets up signal handlers and atexit handlers that call the same code to clean up after itself. In case that the gentle method to terminate the process failed, we still fall back to calling TerminateProcess(), but in that case we now also make sure that processes spawned by the spawned process are terminated; TerminateProcess() does not give the spawned process a chance to do so itself. Please note that this change only affects how Git for Windows tries to terminate processes spawned by Git's own executables. Third-party software that *calls* Git and wants to terminate it *still* need to make sure to imitate this gentle method, otherwise this patch will not have any effect. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/mingw.c | 29 +++++-- compat/win32/exit-process.h | 165 ++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 compat/win32/exit-process.h diff --git a/compat/mingw.c b/compat/mingw.c index aa7525f419cb64..44c63059cd6916 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -13,6 +13,7 @@ #include "symlinks.h" #include "trace2.h" #include "win32.h" +#include "win32/exit-process.h" #include "win32/lazyload.h" #include "wrapper.h" #include @@ -2251,16 +2252,28 @@ int mingw_execvp(const char *cmd, char *const *argv) int mingw_kill(pid_t pid, int sig) { if (pid > 0 && sig == SIGTERM) { - HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid); - - if (TerminateProcess(h, -1)) { + HANDLE h = OpenProcess(PROCESS_CREATE_THREAD | + PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | PROCESS_VM_WRITE | + PROCESS_VM_READ | PROCESS_TERMINATE, + FALSE, pid); + int ret; + + if (h) + ret = exit_process(h, 128 + sig); + else { + h = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + if (!h) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + ret = terminate_process_tree(h, 128 + sig); + } + if (ret) { + errno = err_win_to_posix(GetLastError()); CloseHandle(h); - return 0; } - - errno = err_win_to_posix(GetLastError()); - CloseHandle(h); - return -1; + return ret; } else if (pid > 0 && sig == 0) { HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); if (h) { diff --git a/compat/win32/exit-process.h b/compat/win32/exit-process.h new file mode 100644 index 00000000000000..d53989884cfb0c --- /dev/null +++ b/compat/win32/exit-process.h @@ -0,0 +1,165 @@ +#ifndef EXIT_PROCESS_H +#define EXIT_PROCESS_H + +/* + * This file contains functions to terminate a Win32 process, as gently as + * possible. + * + * At first, we will attempt to inject a thread that calls ExitProcess(). If + * that fails, we will fall back to terminating the entire process tree. + * + * For simplicity, these functions are marked as file-local. + */ + +#include + +/* + * Terminates the process corresponding to the process ID and all of its + * directly and indirectly spawned subprocesses. + * + * This way of terminating the processes is not gentle: the processes get + * no chance of cleaning up after themselves (closing file handles, removing + * .lock files, terminating spawned processes (if any), etc). + */ +static int terminate_process_tree(HANDLE main_process, int exit_status) +{ + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32 entry; + DWORD pids[16384]; + int max_len = sizeof(pids) / sizeof(*pids), i, len, ret = 0; + pid_t pid = GetProcessId(main_process); + + pids[0] = (DWORD)pid; + len = 1; + + /* + * Even if Process32First()/Process32Next() seem to traverse the + * processes in topological order (i.e. parent processes before + * child processes), there is nothing in the Win32 API documentation + * suggesting that this is guaranteed. + * + * Therefore, run through them at least twice and stop when no more + * process IDs were added to the list. + */ + for (;;) { + int orig_len = len; + + memset(&entry, 0, sizeof(entry)); + entry.dwSize = sizeof(entry); + + if (!Process32First(snapshot, &entry)) + break; + + do { + for (i = len - 1; i >= 0; i--) { + if (pids[i] == entry.th32ProcessID) + break; + if (pids[i] == entry.th32ParentProcessID) + pids[len++] = entry.th32ProcessID; + } + } while (len < max_len && Process32Next(snapshot, &entry)); + + if (orig_len == len || len >= max_len) + break; + } + + for (i = len - 1; i > 0; i--) { + HANDLE process = OpenProcess(PROCESS_TERMINATE, FALSE, pids[i]); + + if (process) { + if (!TerminateProcess(process, exit_status)) + ret = -1; + CloseHandle(process); + } + } + if (!TerminateProcess(main_process, exit_status)) + ret = -1; + CloseHandle(main_process); + + return ret; +} + +/** + * Determine whether a process runs in the same architecture as the current + * one. That test is required before we assume that GetProcAddress() returns + * a valid address *for the target process*. + */ +static inline int process_architecture_matches_current(HANDLE process) +{ + static BOOL current_is_wow = -1; + BOOL is_wow; + + if (current_is_wow == -1 && + !IsWow64Process (GetCurrentProcess(), ¤t_is_wow)) + current_is_wow = -2; + if (current_is_wow == -2) + return 0; /* could not determine current process' WoW-ness */ + if (!IsWow64Process (process, &is_wow)) + return 0; /* cannot determine */ + return is_wow == current_is_wow; +} + +/** + * Inject a thread into the given process that runs ExitProcess(). + * + * Note: as kernel32.dll is loaded before any process, the other process and + * this process will have ExitProcess() at the same address. + * + * This function expects the process handle to have the access rights for + * CreateRemoteThread(): PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, + * PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ. + * + * The idea comes from the Dr Dobb's article "A Safer Alternative to + * TerminateProcess()" by Andrew Tucker (July 1, 1999), + * http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547 + * + * If this method fails, we fall back to running terminate_process_tree(). + */ +static int exit_process(HANDLE process, int exit_code) +{ + DWORD code; + + if (GetExitCodeProcess(process, &code) && code == STILL_ACTIVE) { + static int initialized; + static LPTHREAD_START_ROUTINE exit_process_address; + PVOID arg = (PVOID)(intptr_t)exit_code; + DWORD thread_id; + HANDLE thread = NULL; + + if (!initialized) { + HINSTANCE kernel32 = GetModuleHandleA("kernel32"); + if (!kernel32) + die("BUG: cannot find kernel32"); + exit_process_address = + (LPTHREAD_START_ROUTINE)(void (*)(void)) + GetProcAddress(kernel32, "ExitProcess"); + initialized = 1; + } + if (!exit_process_address || + !process_architecture_matches_current(process)) + return terminate_process_tree(process, exit_code); + + thread = CreateRemoteThread(process, NULL, 0, + exit_process_address, + arg, 0, &thread_id); + if (thread) { + CloseHandle(thread); + /* + * If the process survives for 10 seconds (a completely + * arbitrary value picked from thin air), fall back to + * killing the process tree via TerminateProcess(). + */ + if (WaitForSingleObject(process, 10000) == + WAIT_OBJECT_0) { + CloseHandle(process); + return 0; + } + } + + return terminate_process_tree(process, exit_code); + } + + return 0; +} + +#endif From 363f1d8b3a9c90f13ca6fd9ab0dc47e483e3cc9c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 4 Jun 2026 16:24:20 +0000 Subject: [PATCH 18/21] mingw: really handle SIGINT Previously, we did not install any handler for Ctrl+C, but now we really want to because the MSYS2 runtime learned the trick to call the ConsoleCtrlHandler when Ctrl+C was pressed. With this, hitting Ctrl+C while `git log` is running will only terminate the Git process, but not the pager. This finally matches the behavior on Linux and on macOS. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/mingw.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/compat/mingw.c b/compat/mingw.c index 44c63059cd6916..41e055f7de885e 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -3623,7 +3623,14 @@ static void adjust_symlink_flags(void) symlink_file_flags |= 2; symlink_directory_flags |= 2; } +} +static BOOL WINAPI handle_ctrl_c(DWORD ctrl_type) +{ + if (ctrl_type != CTRL_C_EVENT) + return FALSE; /* we did not handle this */ + mingw_raise(SIGINT); + return TRUE; /* we did handle this */ } #ifdef _MSC_VER @@ -3660,6 +3667,8 @@ int wmain(int argc, const wchar_t **wargv) #endif #endif + SetConsoleCtrlHandler(handle_ctrl_c, TRUE); + maybe_redirect_std_handles(); adjust_symlink_flags(); From 0c20c6cb230cf7611ee6b6e18c3aeb13986c462e Mon Sep 17 00:00:00 2001 From: Jayesh Daga Date: Tue, 31 Mar 2026 15:34:26 +0000 Subject: [PATCH 19/21] unpack-trees: use repository from index instead of global unpack_trees() currently initializes its repository from the global 'the_repository', even though a repository instance is already available via the source index. Use 'o->src_index->repo' instead of the global variable, reducing reliance on global repository state. This is a step towards eliminating global repository usage in unpack_trees(). Suggested-by: Patrick Steinhardt Signed-off-by: Jayesh Daga Signed-off-by: Junio C Hamano --- unpack-trees.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/unpack-trees.c b/unpack-trees.c index 998a1e6dc70cae..b42020f16b10ae 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -1780,14 +1780,14 @@ static int clear_ce_flags(struct index_state *istate, xsnprintf(label, sizeof(label), "clear_ce_flags(0x%08lx,0x%08lx)", (unsigned long)select_mask, (unsigned long)clear_mask); - trace2_region_enter("unpack_trees", label, the_repository); + trace2_region_enter("unpack_trees", label, istate->repo); rval = clear_ce_flags_1(istate, istate->cache, istate->cache_nr, &prefix, select_mask, clear_mask, pl, 0, 0); - trace2_region_leave("unpack_trees", label, the_repository); + trace2_region_leave("unpack_trees", label, istate->repo); stop_progress(&istate->progress); return rval; @@ -1882,7 +1882,7 @@ static int verify_absent(const struct cache_entry *, */ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options *o) { - struct repository *repo = the_repository; + struct repository *repo = o->src_index->repo; int i, ret; static struct cache_entry *dfc; struct pattern_list pl; @@ -1903,7 +1903,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options BUG("o->df_conflict_entry is an output only field"); trace_performance_enter(); - trace2_region_enter("unpack_trees", "unpack_trees", the_repository); + trace2_region_enter("unpack_trees", "unpack_trees", repo); prepare_repo_settings(repo); if (repo->settings.command_requires_full_index) { @@ -2007,9 +2007,9 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options } trace_performance_enter(); - trace2_region_enter("unpack_trees", "traverse_trees", the_repository); + trace2_region_enter("unpack_trees", "traverse_trees", repo); ret = traverse_trees(o->src_index, len, t, &info); - trace2_region_leave("unpack_trees", "traverse_trees", the_repository); + trace2_region_leave("unpack_trees", "traverse_trees", repo); trace_performance_leave("traverse_trees"); if (ret < 0) goto return_failed; @@ -2106,7 +2106,7 @@ int unpack_trees(unsigned len, struct tree_desc *t, struct unpack_trees_options dir_clear(o->internal.dir); o->internal.dir = NULL; } - trace2_region_leave("unpack_trees", "unpack_trees", the_repository); + trace2_region_leave("unpack_trees", "unpack_trees", repo); trace_performance_leave("unpack_trees"); return ret; From ff7901eca30c308ef5a448ebd56eaf363b58a02e Mon Sep 17 00:00:00 2001 From: Mirko Faina Date: Wed, 10 Jun 2026 16:38:17 +0200 Subject: [PATCH 20/21] bash-completions: add --max-count-oldest Add missing completion for log --max-count-oldest Signed-off-by: Mirko Faina Signed-off-by: Junio C Hamano --- contrib/completion/git-completion.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index a8e7c6ddbfb2b1..e8757877104eb9 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -2195,7 +2195,7 @@ __git_log_common_options=" --not --all --branches --tags --remotes --first-parent --merges --no-merges - --max-count= + --max-count= --max-count-oldest= --max-age= --since= --after= --min-age= --until= --before= --min-parents= --max-parents= From 0fae78c9d55efe705877ea537fe42c59164ccd94 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 16 Jun 2026 09:00:37 -0700 Subject: [PATCH 21/21] topic flush before -rc1 (batch 2) Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.55.0.adoc | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Documentation/RelNotes/2.55.0.adoc b/Documentation/RelNotes/2.55.0.adoc index 4bada0145d7969..b2adfe51bf19e4 100644 --- a/Documentation/RelNotes/2.55.0.adoc +++ b/Documentation/RelNotes/2.55.0.adoc @@ -70,6 +70,9 @@ UI, Workflows & Features that the user meant "git config set foo.bar baz". Give advice when giving an error message. + * "git rev-list" (and "git log" family of commands) learned a new "--max-count-oldest" + that picks oldest N commits in the range instead of the usual newest. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -188,6 +191,19 @@ Performance, Internal Implementation, Development Support etc. variables into 'repo_config_values' to tie them to a specific repository instance, avoiding cross-repository state leakage. + * Streaming revision walks have been optimized by using a priority queue + for date-sorting commits, speeding up walks repositories with many + merges. + + * A recent regression in t7527 that broke TAP output has been fixed, + some other test noise that also broke TAP output has been silenced, + and 'prove' is now configured to fail on invalid TAP output to + prevent future regressions. + + * A handful of inappropriate uses of the_repository have been + rewritten to use the right repository structure instance in the + unpack-trees.c codepath. + Fixes since v2.54 ----------------- @@ -348,6 +364,27 @@ Fixes since v2.54 has been improved. (merge 4a1eb9304a lo/doc-format-patch-subject-prefix later to maint). + * Advanced emulation of kill() used on Windows in GfW has been + upstreamed to improve the symptoms like left-behind .lock files and + that fails to let the child clean-up itself when it gets killed. + (merge 363f1d8b3a js/win-kill-child-more-gently later to maint). + + * The 'git describe --contains --all' command has been fixed to + properly honor the '--match' and '--exclude' options by passing + them down to 'git name-rev' with the appropriate reference + prefixes. + (merge 1891707d1b jk/describe-contains-all-match-fix later to maint). + + * Various typos, grammatical errors, and duplicated words in both + documentation and code comments have been corrected. + (merge dc6068df67 wy/docs-typofixes later to maint). + + * The subprocess handshake during startup has been made gentler by using + packet_read_line_gently() instead of packet_read_line() to prevent the + parent Git process from dying abruptly when a configured subprocess + (e.g., a clean/smudge filter) fails to start. + (merge 061a68e443 mm/subprocess-handshake-fix later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 80f4b802e9 ja/doc-difftool-synopsis-style later to maint). (merge b96490241e jc/doc-timestamps-in-stat later to maint).