From 8b5636a57ff078ac368f246f813b156552a0726c Mon Sep 17 00:00:00 2001 From: Johannes Sixt Date: Fri, 17 Oct 2025 18:37:52 +0200 Subject: [PATCH 01/22] Revert "gitk: Only restore window size from ~/.gitk, not position" This reverts commit b9bee11526ec (gitk: Only restore window size from ~/.gitk, not position, 2008-03-10). The earlier commit e9937d2a03a4 (Make gitk work reasonably well on Cygwin, 2007-02-01) reworked the window layout considerably. Much of this became irrelevant around 2011 after Cygwin gained an X11 server and switched to a supportable port of the Unix/X11 Tcl/Tk (it is now on the current 8.6 code base). Part of the necessary change was to restore the window size across sessions, but the position was also restored. This raised complaints on the mailing list[*], because Gitk was opened on the wrong monitor. b9bee11526ec was the compromise, because it was only the size that mattered for the Cygwin layout engine to work. I personally, find it annoying when Gitk pops up on a random location on the screen, in particular, since many other applications restore the window positions across sessions, so why not Gitk as well? (I do not operate multi-monitor setups, so I cannot test the case.) [*] https://lore.kernel.org/git/47AAA254.2020008@thorn.ws/ Helped-by: Mark Levedahl Signed-off-by: Johannes Sixt --- gitk | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/gitk b/gitk index 6e4d71d5852533..275f3538117f31 100755 --- a/gitk +++ b/gitk @@ -2764,17 +2764,9 @@ proc makewindow {} { .pwbottom add .bright .ctop add .pwbottom - # restore window width & height if known + # restore window position if known if {[info exists geometry(main)]} { - if {[scan $geometry(main) "%dx%d" w h] >= 2} { - if {$w > [winfo screenwidth .]} { - set w [winfo screenwidth .] - } - if {$h > [winfo screenheight .]} { - set h [winfo screenheight .] - } - wm geometry . "${w}x$h" - } + wm geometry . "$geometry(main)" } if {[info exists geometry(state)] && $geometry(state) eq "zoomed"} { From bf5a55ac5eaef91e87470d704613e6942500a810 Mon Sep 17 00:00:00 2001 From: Johannes Sixt Date: Fri, 17 Oct 2025 18:38:11 +0200 Subject: [PATCH 02/22] gitk: persist position and size of the Tags and Heads window The Tags and Heads window always opens at a default position and size, requiring users to reposition it each time. Remember its geometry between sessions in the config file as `geometry(showrefs)`. Note that the existing configuration is sourced in proc savestuff right before new settings are written. This makes the old settings available as local variables(!) and does not overwrite the current settings. Since we need access to the global geometry(showrefs), it is necessary to unset the local variable. Helped-by: Michael Rappazzo Signed-off-by: Johannes Sixt --- gitk | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/gitk b/gitk index 275f3538117f31..ed616613ae3c91 100755 --- a/gitk +++ b/gitk @@ -2131,12 +2131,14 @@ proc ttk_toplevel {w args} { return $w } -proc make_transient {window origin} { +proc make_transient {window origin {geometry ""}} { wm transient $window $origin - # Windows fails to place transient windows normally, so - # schedule a callback to center them on the parent. - if {[tk windowingsystem] eq {win32}} { + if {$geometry ne ""} { + after idle [list wm geometry $window $geometry] + } elseif {[tk windowingsystem] eq {win32}} { + # Windows fails to place transient windows normally, so + # schedule a callback to center them on the parent. after idle [list tk::PlaceWindow $window widget $origin] } } @@ -3106,6 +3108,11 @@ proc savestuff {w} { puts $f "set geometry(pwsash1) \"[.tf.histframe.pwclist sashpos 1] 1\"" puts $f "set geometry(botwidth) [winfo width .bleft]" puts $f "set geometry(botheight) [winfo height .bleft]" + unset -nocomplain geometry + global geometry + if {[info exists geometry(showrefs)]} { + puts $f "set geometry(showrefs) $geometry(showrefs)" + } array set view_save {} array set views {} @@ -10193,6 +10200,7 @@ proc rmbranch {} { proc showrefs {} { global showrefstop bgcolor fgcolor selectbgcolor global bglist fglist reflistfilter reflist maincursor + global geometry set top .showrefs set showrefstop $top @@ -10203,7 +10211,11 @@ proc showrefs {} { } ttk_toplevel $top wm title $top [mc "Tags and heads: %s" [file tail [pwd]]] - make_transient $top . + if {[info exists geometry(showrefs)]} { + make_transient $top . $geometry(showrefs) + } else { + make_transient $top . + } text $top.list -background $bgcolor -foreground $fgcolor \ -selectbackground $selectbgcolor -font mainfont \ -xscrollcommand "$top.xsb set" -yscrollcommand "$top.ysb set" \ @@ -10239,6 +10251,9 @@ proc showrefs {} { bind $top.list {sel_reflist %W %x %y; break} set reflist {} refill_reflist + # avoid being bound to child windows + bindtags $top [linsert [bindtags $top] 1 bind$top] + bind bind$top {set geometry(showrefs) [wm geometry %W]} } proc sel_reflist {w x y} { From 77e7aab693daee402ef37323715207c5a2daec9f Mon Sep 17 00:00:00 2001 From: Johannes Sixt Date: Thu, 6 Nov 2025 09:20:41 +0100 Subject: [PATCH 03/22] gitk: fix a 'continue' statement outside a loop to 'return' When 5de460a2cfdd (gitk: Refactor per-line part of getblobdiffline and its support) moved the body of a loop into a separate function, several 'continue' statements were changed to 'return'. But one instance was missed. Fix it now. Signed-off-by: Johannes Sixt --- gitk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitk b/gitk index c02db0194d5317..eb657aa1e491b4 100755 --- a/gitk +++ b/gitk @@ -8296,7 +8296,7 @@ proc parseblobdiffline {ids line} { if {![regexp {^diff (--cc|--git) } $line m type]} { set line [convertfrom utf-8 $line] $ctext insert end "$line\n" hunksep - continue + return } # start of a new file set diffinhdr 1 From d445a78873423c55b79f97361a082272acd17f7b Mon Sep 17 00:00:00 2001 From: Johannes Sixt Date: Thu, 6 Nov 2025 10:42:37 +0100 Subject: [PATCH 04/22] gitk: show unescaped file names on 'rename' and 'copy' lines When a file is selected in the file list, the diff window scrolls to the corresponding section. The administrative data needed for this purpose is extracted from the 'rename from', 'rename to', and 'copy to' lines. Escaped file names are unescaped for this purpose. However, the lines shown in the diff window are left in the escaped form. This is not very pleasing. Replace the escaped form by the unescaped form. Add a section to treat the 'copy from' case. Signed-off-by: Johannes Sixt --- gitk | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gitk b/gitk index eb657aa1e491b4..9e4f113c9bc142 100755 --- a/gitk +++ b/gitk @@ -8401,6 +8401,7 @@ proc parseblobdiffline {ids line} { if {$i >= 0} { setinlist difffilestart $i $curdiffstart } + set line "rename from $fname" } elseif {![string compare -length 10 $line "rename to "] || ![string compare -length 8 $line "copy to "]} { set fname [string range $line [expr 4 + [string first " to " $line] ] end] @@ -8408,6 +8409,13 @@ proc parseblobdiffline {ids line} { set fname [lindex $fname 0] } makediffhdr $fname $ids + set line "[lindex $line 0] to $fname" + } elseif {![string compare -length 10 $line "copy from "]} { + set fname [string range $line 10 end] + if {[string index $fname 0] eq "\""} { + set fname [lindex $fname 0] + } + set line "copy from $fname" } elseif {[string compare -length 3 $line "---"] == 0} { # do nothing return From bdb1cf831251b16d174f742178caac181add87f4 Mon Sep 17 00:00:00 2001 From: Tobias Boesch Date: Thu, 6 Nov 2025 14:42:11 +0000 Subject: [PATCH 05/22] gitk: add external diff file rename detection If a file is renamed between commits and an external diff is started through gitk on the original or the renamed file name, gitk is unable to open the renamed file in the external diff editor. It fails to fetch the renamed file from git, because it fetches it using its original path in contrast to using the renamed path of the file. Detect the rename and open the external diff with the original and the renamed file instead of no file (fetch the renamed file path and name from git) no matter if the original or the renamed file is selected in gitk. Signed-off-by: Tobias Boesch Signed-off-by: Johannes Sixt --- gitk | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/gitk b/gitk index bc9efa18566fb8..9659466052e91a 100755 --- a/gitk +++ b/gitk @@ -3806,6 +3806,34 @@ proc external_diff_get_one_file {diffid filename diffdir} { "revision $diffid"] } +proc check_for_renames_in_diff {filepath} { # renames + global difffilestart ctext + + set filename [file tail $filepath] + set renames {} + + foreach loc $difffilestart { + set loclineend [string map {.0 .end} $loc] + set fromlineloc "$loc + 2 lines" + set tolineloc "$loc + 3 lines" + set renfromline [$ctext get $fromlineloc [string map {.0 .end} $fromlineloc]] + set rentoline [$ctext get $tolineloc [string map {.0 .end} $tolineloc]] + if {[string equal -length 12 "rename from " $renfromline] + && [string equal -length 10 "rename to " $rentoline]} { + set renfrom [string range $renfromline 12 end] + set rento [string range $rentoline 10 end] + if {[string first $filename $renfrom] != -1 + || [string first $filename $rento] != -1} { + lappend renames $renfrom + lappend renames $rento + break + } + } + } + + return $renames +} + proc external_diff {} { global nullid nullid2 global flist_menu_file @@ -3836,8 +3864,16 @@ proc external_diff {} { if {$diffdir eq {}} return # gather files to diff - set difffromfile [external_diff_get_one_file $diffidfrom $flist_menu_file $diffdir] - set difftofile [external_diff_get_one_file $diffidto $flist_menu_file $diffdir] + set renames [check_for_renames_in_diff $flist_menu_file] + set renamefrom [lindex $renames 0] + set renameto [lindex $renames 1] + if {$renamefrom ne {} && $renameto ne {}} { + set difffromfile [external_diff_get_one_file $diffidfrom $renamefrom $diffdir] + set difftofile [external_diff_get_one_file $diffidto $renameto $diffdir] + } else { + set difffromfile [external_diff_get_one_file $diffidfrom $flist_menu_file $diffdir] + set difftofile [external_diff_get_one_file $diffidto $flist_menu_file $diffdir] + } if {$difffromfile ne {} && $difftofile ne {}} { set cmd [list [shellsplit $extdifftool] $difffromfile $difftofile] From 881793c4f71c84e70af256c5721475c7c088b3f7 Mon Sep 17 00:00:00 2001 From: Antonin Delpeuch Date: Mon, 17 Nov 2025 08:04:31 +0000 Subject: [PATCH 06/22] xdiff: add 'minimal' to XDF_DIFF_ALGORITHM_MASK The XDF_DIFF_ALGORITHM_MASK bit mask only includes bits for the patience and histogram diffs, not for the minimal one. This means that when reseting the diff algorithm to the default one, one needs to separately clear the bit for the minimal diff. There are places in the code that fail to do that: merge-ort.c and builtin/merge-file.c. Add the XDF_NEED_MINIMAL bit to the bit mask, and remove the separate clearing of this bit in the places where it hasn't been forgotten. Signed-off-by: Antonin Delpeuch Signed-off-by: Junio C Hamano --- diff.c | 1 - merge-ort.c | 1 - xdiff/xdiff.h | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/diff.c b/diff.c index a74e701806be52..0eec4a2fe8ceee 100644 --- a/diff.c +++ b/diff.c @@ -3527,7 +3527,6 @@ static int set_diff_algorithm(struct diff_options *opts, return -1; /* clear out previous settings */ - DIFF_XDL_CLR(opts, NEED_MINIMAL); opts->xdl_opts &= ~XDF_DIFF_ALGORITHM_MASK; opts->xdl_opts |= value; diff --git a/merge-ort.c b/merge-ort.c index 29858074f9d8bf..23e2b64c79bf27 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -5496,7 +5496,6 @@ int parse_merge_opt(struct merge_options *opt, const char *s) if (value < 0) return -1; /* clear out previous settings */ - DIFF_XDL_CLR(opt, NEED_MINIMAL); opt->xdl_opts &= ~XDF_DIFF_ALGORITHM_MASK; opt->xdl_opts |= value; } diff --git a/xdiff/xdiff.h b/xdiff/xdiff.h index 2cecde5afe5da1..dc370712e92860 100644 --- a/xdiff/xdiff.h +++ b/xdiff/xdiff.h @@ -43,7 +43,7 @@ extern "C" { #define XDF_PATIENCE_DIFF (1 << 14) #define XDF_HISTOGRAM_DIFF (1 << 15) -#define XDF_DIFF_ALGORITHM_MASK (XDF_PATIENCE_DIFF | XDF_HISTOGRAM_DIFF) +#define XDF_DIFF_ALGORITHM_MASK (XDF_PATIENCE_DIFF | XDF_HISTOGRAM_DIFF | XDF_NEED_MINIMAL) #define XDF_DIFF_ALG(x) ((x) & XDF_DIFF_ALGORITHM_MASK) #define XDF_INDENT_HEURISTIC (1 << 23) From ffffb987fcd3b3d6b88aceed87000ef4a5b6114e Mon Sep 17 00:00:00 2001 From: Antonin Delpeuch Date: Mon, 17 Nov 2025 08:04:32 +0000 Subject: [PATCH 07/22] blame: make diff algorithm configurable The diff algorithm used in 'git-blame(1)' is set to 'myers', without the possibility to change it aside from the `--minimal` option. There has been long-standing interest in changing the default diff algorithm to "histogram", and Git 3.0 was floated as a possible occasion for taking some steps towards that: https://lore.kernel.org/git/xmqqed873vgn.fsf@gitster.g/ As a preparation for this move, it is worth making sure that the diff algorithm is configurable where useful. Make it configurable in the `git-blame(1)` command by introducing the `--diff-algorithm` option and make honor the `diff.algorithm` config variable. Keep Myers diff as the default. Signed-off-by: Antonin Delpeuch Signed-off-by: Junio C Hamano --- Documentation/diff-algorithm-option.adoc | 20 +++ Documentation/diff-options.adoc | 21 +-- Documentation/git-blame.adoc | 2 + builtin/blame.c | 52 +++++- t/meson.build | 1 + t/t8015-blame-diff-algorithm.sh | 203 +++++++++++++++++++++++ 6 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 Documentation/diff-algorithm-option.adoc create mode 100755 t/t8015-blame-diff-algorithm.sh diff --git a/Documentation/diff-algorithm-option.adoc b/Documentation/diff-algorithm-option.adoc new file mode 100644 index 00000000000000..8e3a0b63d784d8 --- /dev/null +++ b/Documentation/diff-algorithm-option.adoc @@ -0,0 +1,20 @@ +`--diff-algorithm=(patience|minimal|histogram|myers)`:: + Choose a diff algorithm. The variants are as follows: ++ +-- + `default`;; + `myers`;; + The basic greedy diff algorithm. Currently, this is the default. + `minimal`;; + Spend extra time to make sure the smallest possible diff is + produced. + `patience`;; + Use "patience diff" algorithm when generating patches. + `histogram`;; + This algorithm extends the patience algorithm to "support + low-occurrence common elements". +-- ++ +For instance, if you configured the `diff.algorithm` variable to a +non-default value and want to use the default one, then you +have to use `--diff-algorithm=default` option. diff --git a/Documentation/diff-options.adoc b/Documentation/diff-options.adoc index ae31520f7f1d13..9cdad6f72a0c7d 100644 --- a/Documentation/diff-options.adoc +++ b/Documentation/diff-options.adoc @@ -197,26 +197,7 @@ and starts with __, this algorithm attempts to prevent it from appearing as a deletion or addition in the output. It uses the "patience diff" algorithm internally. -`--diff-algorithm=(patience|minimal|histogram|myers)`:: - Choose a diff algorithm. The variants are as follows: -+ --- - `default`;; - `myers`;; - The basic greedy diff algorithm. Currently, this is the default. - `minimal`;; - Spend extra time to make sure the smallest possible diff is - produced. - `patience`;; - Use "patience diff" algorithm when generating patches. - `histogram`;; - This algorithm extends the patience algorithm to "support - low-occurrence common elements". --- -+ -For instance, if you configured the `diff.algorithm` variable to a -non-default value and want to use the default one, then you -have to use `--diff-algorithm=default` option. +include::diff-algorithm-option.adoc[] `--stat[=[,[,]]]`:: Generate a diffstat. By default, as much space as necessary diff --git a/Documentation/git-blame.adoc b/Documentation/git-blame.adoc index e438d286258826..adcbb6f5dc97a3 100644 --- a/Documentation/git-blame.adoc +++ b/Documentation/git-blame.adoc @@ -85,6 +85,8 @@ include::blame-options.adoc[] Ignore whitespace when comparing the parent's version and the child's to find where the lines came from. +include::diff-algorithm-option.adoc[] + --abbrev=:: Instead of using the default 7+1 hexadecimal digits as the abbreviated object name, use +1 digits, where is at diff --git a/builtin/blame.c b/builtin/blame.c index 2703820258d5f9..27b513d27faddd 100644 --- a/builtin/blame.c +++ b/builtin/blame.c @@ -779,6 +779,19 @@ static int git_blame_config(const char *var, const char *value, } } + if (!strcmp(var, "diff.algorithm")) { + long diff_algorithm; + if (!value) + return config_error_nonbool(var); + diff_algorithm = parse_algorithm_value(value); + if (diff_algorithm < 0) + return error(_("unknown value for config '%s': %s"), + var, value); + xdl_opts &= ~XDF_DIFF_ALGORITHM_MASK; + xdl_opts |= diff_algorithm; + return 0; + } + if (git_diff_heuristic_config(var, value, cb) < 0) return -1; if (userdiff_config(var, value) < 0) @@ -824,6 +837,38 @@ static int blame_move_callback(const struct option *option, const char *arg, int return 0; } +static int blame_diff_algorithm_minimal(const struct option *option, + const char *arg, int unset) +{ + int *opt = option->value; + + BUG_ON_OPT_ARG(arg); + + *opt &= ~XDF_DIFF_ALGORITHM_MASK; + if (!unset) + *opt |= XDF_NEED_MINIMAL; + + return 0; +} + +static int blame_diff_algorithm_callback(const struct option *option, + const char *arg, int unset) +{ + int *opt = option->value; + long value = parse_algorithm_value(arg); + + BUG_ON_OPT_NEG(unset); + + if (value < 0) + return error(_("option diff-algorithm accepts \"myers\", " + "\"minimal\", \"patience\" and \"histogram\"")); + + *opt &= ~XDF_DIFF_ALGORITHM_MASK; + *opt |= value; + + return 0; +} + static int is_a_rev(const char *name) { struct object_id oid; @@ -915,11 +960,16 @@ int cmd_blame(int argc, OPT_BIT('s', NULL, &output_option, N_("suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR), OPT_BIT('e', "show-email", &output_option, N_("show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL), OPT_BIT('w', NULL, &xdl_opts, N_("ignore whitespace differences"), XDF_IGNORE_WHITESPACE), + OPT_CALLBACK_F(0, "diff-algorithm", &xdl_opts, N_(""), + N_("choose a diff algorithm"), + PARSE_OPT_NONEG, blame_diff_algorithm_callback), OPT_STRING_LIST(0, "ignore-rev", &ignore_rev_list, N_("rev"), N_("ignore when blaming")), OPT_STRING_LIST(0, "ignore-revs-file", &ignore_revs_file_list, N_("file"), N_("ignore revisions from ")), OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE), OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR), - OPT_BIT(0, "minimal", &xdl_opts, N_("spend extra cycles to find better match"), XDF_NEED_MINIMAL), + OPT_CALLBACK_F(0, "minimal", &xdl_opts, NULL, + N_("spend extra cycles to find a better match"), + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, blame_diff_algorithm_minimal), OPT_STRING('S', NULL, &revs_file, N_("file"), N_("use revisions from instead of calling git-rev-list")), OPT_STRING(0, "contents", &contents_from, N_("file"), N_("use 's contents as the final image")), OPT_CALLBACK_F('C', NULL, &opt, N_("score"), N_("find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback), diff --git a/t/meson.build b/t/meson.build index 401b24e50e0499..9f2fe7af8ba4c9 100644 --- a/t/meson.build +++ b/t/meson.build @@ -955,6 +955,7 @@ integration_tests = [ 't8012-blame-colors.sh', 't8013-blame-ignore-revs.sh', 't8014-blame-ignore-fuzzy.sh', + 't8015-blame-diff-algorithm.sh', 't8020-last-modified.sh', 't9001-send-email.sh', 't9002-column.sh', diff --git a/t/t8015-blame-diff-algorithm.sh b/t/t8015-blame-diff-algorithm.sh new file mode 100755 index 00000000000000..cd709536c6ecb1 --- /dev/null +++ b/t/t8015-blame-diff-algorithm.sh @@ -0,0 +1,203 @@ +#!/bin/sh + +test_description='git blame with specific diff algorithm' + +. ./test-lib.sh + +test_expect_success setup ' + cat >file.c <<-\EOF && + int f(int x, int y) + { + if (x == 0) + { + return y; + } + return x; + } + + int g(size_t u) + { + while (u < 30) + { + u++; + } + return u; + } + EOF + test_write_lines x x x x >file.txt && + git add file.c file.txt && + GIT_AUTHOR_NAME=Commit_1 git commit -m Commit_1 && + + cat >file.c <<-\EOF && + int g(size_t u) + { + while (u < 30) + { + u++; + } + return u; + } + + int h(int x, int y, int z) + { + if (z == 0) + { + return x; + } + return y; + } + EOF + test_write_lines x x x A B C D x E F G >file.txt && + git add file.c file.txt && + GIT_AUTHOR_NAME=Commit_2 git commit -m Commit_2 +' + +test_expect_success 'blame uses Myers diff algorithm by default' ' + cat >expected <<-\EOF && + Commit_2 int g(size_t u) + Commit_1 { + Commit_2 while (u < 30) + Commit_1 { + Commit_2 u++; + Commit_1 } + Commit_2 return u; + Commit_1 } + Commit_1 + Commit_2 int h(int x, int y, int z) + Commit_1 { + Commit_2 if (z == 0) + Commit_1 { + Commit_2 return x; + Commit_1 } + Commit_2 return y; + Commit_1 } + EOF + + git blame file.c >output && + sed -e "s/^[^ ]* (\([^ ]*\) [^)]*)/\1/g" output >without_varying_parts && + sed -e "s/ *$//g" without_varying_parts >actual && + test_cmp expected actual +' + +test_expect_success 'blame honors --diff-algorithm option' ' + cat >expected <<-\EOF && + Commit_1 int g(size_t u) + Commit_1 { + Commit_1 while (u < 30) + Commit_1 { + Commit_1 u++; + Commit_1 } + Commit_1 return u; + Commit_1 } + Commit_2 + Commit_2 int h(int x, int y, int z) + Commit_2 { + Commit_2 if (z == 0) + Commit_2 { + Commit_2 return x; + Commit_2 } + Commit_2 return y; + Commit_2 } + EOF + + git blame file.c --diff-algorithm histogram >output && + sed -e "s/^[^ ]* (\([^ ]*\) [^)]*)/\1/g" output >without_varying_parts && + sed -e "s/ *$//g" without_varying_parts >actual && + test_cmp expected actual +' + +test_expect_success 'blame honors diff.algorithm config variable' ' + cat >expected <<-\EOF && + Commit_1 int g(size_t u) + Commit_1 { + Commit_1 while (u < 30) + Commit_1 { + Commit_1 u++; + Commit_1 } + Commit_1 return u; + Commit_1 } + Commit_2 + Commit_2 int h(int x, int y, int z) + Commit_2 { + Commit_2 if (z == 0) + Commit_2 { + Commit_2 return x; + Commit_2 } + Commit_2 return y; + Commit_2 } + EOF + + git -c diff.algorithm=histogram blame file.c >output && + sed -e "s/^[^ ]* (\([^ ]*\) [^)]*)/\1/g" \ + -e "s/ *$//g" output >actual && + test_cmp expected actual +' + +test_expect_success 'blame gives priority to --diff-algorithm over diff.algorithm' ' + cat >expected <<-\EOF && + Commit_1 int g(size_t u) + Commit_1 { + Commit_1 while (u < 30) + Commit_1 { + Commit_1 u++; + Commit_1 } + Commit_1 return u; + Commit_1 } + Commit_2 + Commit_2 int h(int x, int y, int z) + Commit_2 { + Commit_2 if (z == 0) + Commit_2 { + Commit_2 return x; + Commit_2 } + Commit_2 return y; + Commit_2 } + EOF + + git -c diff.algorithm=myers blame file.c --diff-algorithm histogram >output && + sed -e "s/^[^ ]* (\([^ ]*\) [^)]*)/\1/g" \ + -e "s/ *$//g" output >actual && + test_cmp expected actual +' + +test_expect_success 'blame honors --minimal option' ' + cat >expected <<-\EOF && + Commit_1 x + Commit_1 x + Commit_1 x + Commit_2 A + Commit_2 B + Commit_2 C + Commit_2 D + Commit_1 x + Commit_2 E + Commit_2 F + Commit_2 G + EOF + + git blame file.txt --minimal >output && + sed -e "s/^[^ ]* (\([^ ]*\) [^)]*)/\1/g" output >actual && + test_cmp expected actual +' + +test_expect_success 'blame respects the order of diff options' ' + cat >expected <<-\EOF && + Commit_1 x + Commit_1 x + Commit_1 x + Commit_2 A + Commit_2 B + Commit_2 C + Commit_2 D + Commit_2 x + Commit_2 E + Commit_2 F + Commit_2 G + EOF + + git blame file.txt --minimal --diff-algorithm myers >output && + sed -e "s/^[^ ]* (\([^ ]*\) [^)]*)/\1/g" output >actual && + test_cmp expected actual +' + +test_done From f18aa68861538e93421699aa366d6691a85258b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Scharfe?= Date: Mon, 17 Nov 2025 20:42:55 +0100 Subject: [PATCH 08/22] wrapper: simplify xmkstemp() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Call xmkstemp_mode() instead of duplicating its error handling code. This switches the implementation from the system's mkstemp(3) to our own git_mkstemp_mode(), which works just as well. Signed-off-by: René Scharfe Signed-off-by: Junio C Hamano --- wrapper.c | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/wrapper.c b/wrapper.c index 2f00d2ac876c16..bfe7e30f0c80e8 100644 --- a/wrapper.c +++ b/wrapper.c @@ -421,24 +421,7 @@ FILE *fopen_or_warn(const char *path, const char *mode) int xmkstemp(char *filename_template) { - int fd; - char origtemplate[PATH_MAX]; - strlcpy(origtemplate, filename_template, sizeof(origtemplate)); - - fd = mkstemp(filename_template); - if (fd < 0) { - int saved_errno = errno; - const char *nonrelative_template; - - if (strlen(filename_template) != strlen(origtemplate)) - filename_template = origtemplate; - - nonrelative_template = absolute_path(filename_template); - errno = saved_errno; - die_errno("Unable to create temporary file '%s'", - nonrelative_template); - } - return fd; + return xmkstemp_mode(filename_template, 0600); } /* Adapted from libiberty's mkstemp.c. */ From ffe702b3edf85aa924d685a8603205d47e94e851 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Mon, 3 Nov 2025 18:01:46 +0000 Subject: [PATCH 09/22] t6429: update comment to mention correct tool A comment at the top of t6429 mentions why the test doesn't exercise git rebase or git cherry-pick. However, it claims that it uses `test-tool fast-rebase`. That was true when the comment was written, but commit f920b0289ba3 (replay: introduce new builtin, 2023-11-24) changed it to use git replay without updating this comment. We could potentially just strike this second comment, since git replay is a bona fide built-in, but perhaps the explanation about why it focuses on git replay is still useful. Update the comment to make it accurate again. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- t/t6429-merge-sequence-rename-caching.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh index 0f39ed0d08a342..dcb734b10b3e2b 100755 --- a/t/t6429-merge-sequence-rename-caching.sh +++ b/t/t6429-merge-sequence-rename-caching.sh @@ -11,14 +11,13 @@ test_description="remember regular & dir renames in sequence of merges" # sure that we are triggering rename caching rather than rename # bypassing. # -# NOTE 2: this testfile uses 'test-tool fast-rebase' instead of either -# cherry-pick or rebase. sequencer.c is only superficially -# integrated with merge-ort; it calls merge_switch_to_result() -# after EACH merge, which updates the index and working copy AND -# throws away the cached results (because merge_switch_to_result() -# is only supposed to be called at the end of the sequence). -# Integrating them more deeply is a big task, so for now the tests -# use 'test-tool fast-rebase'. +# NOTE 2: this testfile uses replay instead of either cherry-pick or rebase. +# sequencer.c is only superficially integrated with merge-ort; it +# calls merge_switch_to_result() after EACH merge, which updates the +# index and working copy AND throws away the cached results (because +# merge_switch_to_result() is only supposed to be called at the end +# of the sequence). Integrating them more deeply is a big task, so +# for now the tests use 'git replay'. # From d5663a4b05640a44aa52a0cc32ba6a601d7c9149 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Mon, 3 Nov 2025 18:01:47 +0000 Subject: [PATCH 10/22] merge-ort: remove debugging crud While developing commit a16e8efe5c2b (merge-ort: fix merge.directoryRenames=false, 2025-03-13), I was testing things out and had an extra condition on one of the if-blocks that I occasionally swapped between '&& 0' and '&& 1' to see the effects of the changes. I forgot to remove it before submitting and it wasn't caught in review. Remove it now. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/merge-ort.c b/merge-ort.c index 29858074f9d8bf..23b55c5b929b22 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -3438,7 +3438,7 @@ static int collect_renames(struct merge_options *opt, continue; } if (opt->detect_directory_renames == MERGE_DIRECTORY_RENAMES_NONE && - p->status == 'R' && 1) { + p->status == 'R') { possibly_cache_new_pair(renames, p, side_index, NULL); goto skip_directory_renames; } From a562d90a350dcbddc8794809aef9608467022e34 Mon Sep 17 00:00:00 2001 From: Elijah Newren Date: Mon, 3 Nov 2025 18:01:48 +0000 Subject: [PATCH 11/22] merge-ort: fix failing merges in special corner case At GitHub, we had a repository that was triggering git: merge-ort.c:3032: process_renames: Assertion `newinfo && !newinfo->merged.clean` failed. during git replay. This sounds similar to the somewhat recent f6ecb603ff8a (merge-ort: fix directory rename on top of source of other rename/delete, 2025-08-06), but the cause is different. Unlike that case, there are no rename-to-self situations arising in this case, and new to this case it can only be triggered during a replay operation on the 2nd or later commit being replayed, never on the first merge in the sequence. To trigger, the repository needs: * an upstream which: * renames a file to a different directory, e.g. old/file -> new/file * leaves other files remaining in the original directory (so that e.g. "old/" still exists upstream even though file has been removed from it and placed elsewhere) * a topic branch being rebased where: * a commit in the sequence: * modifies old/file * a subsequent commit in the sequence being replayed: * does NOT touch *anything* under new/ * does NOT touch old/file * DOES modify other paths under old/ * does NOT have any relevant renames that we need to detect _anywhere_ elsewhere in the tree (meaning this interacts interestingly with both directory renames and cached renames) In such a case, the assertion will trigger. The fix turns out to be surprisingly simple. I have a very vague recollection that I actually considered whether to add such an if-check years ago when I added the very similar one for oldinfo in 1b6b902d95a5 (merge-ort: process_renames() now needs more defensiveness, 2021-01-19), but I think I couldn't figure out a possible way to trigger it and was worried at the time that if I didn't know how to trigger it then I wasn't so sure that simply skipping it was correct. Waiting did give me a chance to put more thorough tests and checks into place for the rename-to-self cases a few months back, which I might not have found as easily otherwise. Anyway, put the check in place now and add a test that demonstrates the fix. Note that this bug, as demonstrated by the conditions listed above, runs at the intersection of relevant renames, trivial directory resolutions, and cached renames. All three of those optimizations are ones that unfortunately make the code (and testcases!) a bit more complex, and threading all three makes it a bit more so. However, the testcase isn't crazy enough that I'd expect no one to ever hit it in practice, and was confused why we didn't see it before. After some digging, I discovered that merge.directoryRenames=false is a workaround to this bug, and GitHub used that setting until recently (it was a "temporary" match-what-libgit2-does piece of code that lasted years longer than intended). Since the conditions I gave above for triggering this bug rule out the possibility of there being directory renames, one might assume that it shouldn't matter whether you try to detect such renames if there aren't any. However, due to commit a16e8efe5c2b (merge-ort: fix merge.directoryRenames=false, 2025-03-13), the heavy hammer used there means that merge.directoryRenames=false ALSO turns off rename caching, which is critical to triggering the bug. This becomes a bit more than an aside since... Re-reading that old commit, a16e8efe5c2b (merge-ort: fix merge.directoryRenames=false, 2025-03-13), it appears that the solution to this latest bug might have been at least a partial alternative solution to that old commit. And it may have been an improved alternative (or at least help implement one), since it may be able to avoid the heavy-handed disabling of rename cache. That might be an interesting future thing to investigate, but is not critical for the current fix. However, since I spent time digging it all up, at least leave a small comment tweak breadcrumb to help some future reader (myself or others) who wants to dig further to connect the dots a little quicker. Signed-off-by: Elijah Newren Signed-off-by: Junio C Hamano --- merge-ort.c | 29 ++++++++- t/t6429-merge-sequence-rename-caching.sh | 78 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/merge-ort.c b/merge-ort.c index 23b55c5b929b22..a1f3333e44a1ef 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -2912,6 +2912,32 @@ static int process_renames(struct merge_options *opt, if (!oldinfo || oldinfo->merged.clean) continue; + /* + * Rename caching from a previous commit might give us an + * irrelevant rename for the current commit. + * + * Imagine: + * foo/A -> bar/A + * was a cached rename for the upstream side from the + * previous commit (without the directories being renamed), + * but the next commit being replayed + * * does NOT add or delete files + * * does NOT have directory renames + * * does NOT modify any files under bar/ + * * does NOT modify foo/A + * * DOES modify other files under foo/ (otherwise the + * !oldinfo check above would have already exited for + * us) + * In such a case, our trivial directory resolution will + * have already merged bar/, and our attempt to process + * the cached + * foo/A -> bar/A + * would be counterproductive, and lack the necessary + * information anyway. Skip such renames. + */ + if (!newinfo) + continue; + /* * diff_filepairs have copies of pathnames, thus we have to * use standard 'strcmp()' (negated) instead of '=='. @@ -5118,7 +5144,8 @@ static void merge_check_renames_reusable(struct merge_options *opt, * optimization" comment near that case). * * This could be revisited in the future; see the commit message - * where this comment was added for some possible pointers. + * where this comment was added for some possible pointers, or the + * later commit where this comment was added. */ if (opt->detect_directory_renames == MERGE_DIRECTORY_RENAMES_NONE) { renames->cached_pairs_valid_side = 0; /* neither side valid */ diff --git a/t/t6429-merge-sequence-rename-caching.sh b/t/t6429-merge-sequence-rename-caching.sh index dcb734b10b3e2b..15dd2d94b75f0a 100755 --- a/t/t6429-merge-sequence-rename-caching.sh +++ b/t/t6429-merge-sequence-rename-caching.sh @@ -768,4 +768,82 @@ test_expect_success 'avoid assuming we detected renames' ' ) ' +# +# In the following testcase: +# Base: olddir/{valuesX_1, valuesY_1, valuesZ_1} +# other/content +# Upstream: rename olddir/valuesX_1 -> newdir/valuesX_2 +# Topic_1: modify olddir/valuesX_1 -> olddir/valuesX_3 +# Topic_2: modify olddir/valuesY, +# modify other/content +# Expected Pick1: olddir/{valuesY, valuesZ}, newdir/valuesX, other/content +# Expected Pick2: olddir/{valuesY, valuesZ}, newdir/valuesX, other/content +# +# This testcase presents no problems for git traditionally, but the fact that +# olddir/valuesX -> newdir/valuesX +# gets cached after the first pick presents a problem for the second commit to +# be replayed, because it appears to be an irrelevant rename, so the trivial +# directory resolution will resolve newdir/ without recursing into it, giving +# us no way to apply the cached rename to anything. +# +test_expect_success 'rename a file, use it on first pick, but irrelevant on second' ' + git init rename_a_file_use_it_once_irrelevant_on_second && + ( + cd rename_a_file_use_it_once_irrelevant_on_second && + + mkdir olddir/ other/ && + test_seq 3 8 >olddir/valuesX && + test_seq 3 8 >olddir/valuesY && + test_seq 3 8 >olddir/valuesZ && + printf "%s\n" A B C D E F G >other/content && + git add olddir other && + git commit -m orig && + + git branch upstream && + git branch topic && + + git switch upstream && + test_seq 1 8 >olddir/valuesX && + git add olddir && + mkdir newdir && + git mv olddir/valuesX newdir && + git commit -m "Renamed (and modified) olddir/valuesX into newdir/" && + + git switch topic && + + test_seq 3 10 >olddir/valuesX && + git add olddir && + git commit -m A && + + test_seq 1 8 >olddir/valuesY && + printf "%s\n" A B C D E F G H I >other/content && + git add olddir/valuesY other && + git commit -m B && + + # + # Actual testing; mostly we want to verify that we do not hit + # git: merge-ort.c:3032: process_renames: Assertion `newinfo && !newinfo->merged.clean` failed. + # + + git switch upstream && + git config merge.directoryRenames true && + + git replay --onto HEAD upstream~1..topic >out && + + # + # ...but we may as well check that the replay gave us a reasonable result + # + + git update-ref --stdin tracked && + test_line_count = 4 tracked && + test_path_is_file newdir/valuesX && + test_path_is_file olddir/valuesY && + test_path_is_file olddir/valuesZ && + test_path_is_file other/content + ) +' + test_done From d22a488482092da64ad19fda82edde199bed2466 Mon Sep 17 00:00:00 2001 From: David Macek Date: Mon, 17 Nov 2025 20:39:44 +0000 Subject: [PATCH 12/22] wincred: avoid memory corruption `wcsncpy_s()` wants to write the terminating null character so we need to allocate one more space for it in the target memory block. This should fix crashes when trying to read passwords. When this happened, the password/token wouldn't print out and Git would therefore ask for a new password every time. Signed-off-by: David Macek Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- contrib/credential/wincred/git-credential-wincred.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c index 5683846b4b4d1f..73c2b9b72ab53e 100644 --- a/contrib/credential/wincred/git-credential-wincred.c +++ b/contrib/credential/wincred/git-credential-wincred.c @@ -165,7 +165,7 @@ static void get_credential(void) write_item("username", creds[i]->UserName, creds[i]->UserName ? wcslen(creds[i]->UserName) : 0); if (creds[i]->CredentialBlobSize > 0) { - secret = xmalloc(creds[i]->CredentialBlobSize); + secret = xmalloc(creds[i]->CredentialBlobSize + sizeof(WCHAR)); wcsncpy_s(secret, creds[i]->CredentialBlobSize, (LPCWSTR)creds[i]->CredentialBlob, creds[i]->CredentialBlobSize / sizeof(WCHAR)); line = wcstok_s(secret, L"\r\n", &remaining_lines); write_item("password", line, line ? wcslen(line) : 0); From b0d5c88cca3d67fd020d1e71fb04380cd15f5e55 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 17 Nov 2025 20:40:08 +0000 Subject: [PATCH 13/22] cmake: stop trying to build the reftable and xdiff libraries In the `en/make-libgit-a` topic branch, more precisely in the commits f3b4c89d59f1 (make: delete REFTABLE_LIB, add reftable to LIB_OBJS, 2025-10-02) and cf680cdb9543 (make: delete XDIFF_LIB, add xdiff to LIB_OBJS, 2025-10-02), the strategy to build three static libraries was rethought, and instead only one static library is now built. This is good. However, the CMake definition was not changed accordingly, and now CMake-based builds fail thusly: [...] Generating hook-list.h CMake Error at CMakeLists.txt:122 (string): string sub-command REPLACE requires at least four arguments. Call Stack (most recent call first): CMakeLists.txt:711 (parse_makefile_for_sources) CMake Error at CMakeLists.txt:122 (string): string sub-command REPLACE requires at least four arguments. Call Stack (most recent call first): CMakeLists.txt:717 (parse_makefile_for_sources) -- Configuring incomplete, errors occurred! Fix that by removing the parts that expect the reftable and xdiff objects to be defined separately in the Makefile, still. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- contrib/buildsystems/CMakeLists.txt | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index edb0fc04ad7649..479163ab5cd3b5 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -679,18 +679,6 @@ list(APPEND libgit_SOURCES "${CMAKE_BINARY_DIR}/version-def.h") add_library(libgit ${libgit_SOURCES} ${compat_SOURCES}) -#libxdiff -parse_makefile_for_sources(libxdiff_SOURCES ${CMAKE_SOURCE_DIR}/Makefile "XDIFF_OBJS") - -list(TRANSFORM libxdiff_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") -add_library(xdiff STATIC ${libxdiff_SOURCES}) - -#reftable -parse_makefile_for_sources(reftable_SOURCES ${CMAKE_SOURCE_DIR}/Makefile "REFTABLE_OBJS") - -list(TRANSFORM reftable_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") -add_library(reftable STATIC ${reftable_SOURCES}) - if(WIN32) add_custom_command(OUTPUT ${CMAKE_BINARY_DIR}/git.rc COMMAND "${SH_EXE}" "${CMAKE_SOURCE_DIR}/GIT-VERSION-GEN" @@ -720,7 +708,7 @@ endif() #link all required libraries to common-main add_library(common-main OBJECT ${CMAKE_SOURCE_DIR}/common-main.c) -target_link_libraries(common-main libgit xdiff reftable ${ZLIB_LIBRARIES}) +target_link_libraries(common-main libgit ${ZLIB_LIBRARIES}) if(Intl_FOUND) target_link_libraries(common-main ${Intl_LIBRARIES}) endif() From af3919816f20c7c54e6d377945b2f6344b3588fe Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 17 Nov 2025 20:46:14 +0000 Subject: [PATCH 14/22] mingw: avoid the comma operator The pattern `return errno = ..., -1;` is observed several times in `compat/mingw.c`. It has served us well over the years, but now clang starts complaining: compat/mingw.c:723:24: error: possible misuse of comma operator here [-Werror,-Wcomma] 723 | return errno = ENOSYS, -1; | ^ See for example this failing workflow run: https://github.com/git-for-windows/git-sdk-arm64/actions/runs/15457893907/job/43513458823#step:8:201 Let's appease clang (and also reduce the use of the no longer common comma operator). Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/mingw.c | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index 8538e3d1729d25..9b894d9639b642 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -491,8 +491,10 @@ static int mingw_open_append(wchar_t const *wfilename, int oflags, ...) DWORD create = (oflags & O_CREAT) ? OPEN_ALWAYS : OPEN_EXISTING; /* only these flags are supported */ - if ((oflags & ~O_CREAT) != (O_WRONLY | O_APPEND)) - return errno = ENOSYS, -1; + if ((oflags & ~O_CREAT) != (O_WRONLY | O_APPEND)) { + errno = ENOSYS; + return -1; + } /* * FILE_SHARE_WRITE is required to permit child processes @@ -2450,12 +2452,14 @@ static int start_timer_thread(void) timer_event = CreateEvent(NULL, FALSE, FALSE, NULL); if (timer_event) { timer_thread = (HANDLE) _beginthreadex(NULL, 0, ticktack, NULL, 0, NULL); - if (!timer_thread ) - return errno = ENOMEM, - error("cannot start timer thread"); - } else - return errno = ENOMEM, - error("cannot allocate resources for timer"); + if (!timer_thread ) { + errno = ENOMEM; + return error("cannot start timer thread"); + } + } else { + errno = ENOMEM; + return error("cannot allocate resources for timer"); + } return 0; } @@ -2488,13 +2492,15 @@ int setitimer(int type UNUSED, struct itimerval *in, struct itimerval *out) static const struct timeval zero; static int atexit_done; - if (out) - return errno = EINVAL, - error("setitimer param 3 != NULL not implemented"); + if (out) { + errno = EINVAL; + return error("setitimer param 3 != NULL not implemented"); + } if (!is_timeval_eq(&in->it_interval, &zero) && - !is_timeval_eq(&in->it_interval, &in->it_value)) - return errno = EINVAL, - error("setitimer: it_interval must be zero or eq it_value"); + !is_timeval_eq(&in->it_interval, &in->it_value)) { + errno = EINVAL; + return error("setitimer: it_interval must be zero or eq it_value"); + } if (timer_thread) stop_timer_thread(); @@ -2516,12 +2522,14 @@ int sigaction(int sig, struct sigaction *in, struct sigaction *out) { if (sig == SIGCHLD) return -1; - else if (sig != SIGALRM) - return errno = EINVAL, - error("sigaction only implemented for SIGALRM"); - if (out) - return errno = EINVAL, - error("sigaction: param 3 != NULL not implemented"); + else if (sig != SIGALRM) { + errno = EINVAL; + return error("sigaction only implemented for SIGALRM"); + } + if (out) { + errno = EINVAL; + return error("sigaction: param 3 != NULL not implemented"); + } timer_fn = in->sa_handler; return 0; From cd99203f86ea32e0b84d1ef18f5148b74972f617 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 Nov 2025 02:26:45 -0500 Subject: [PATCH 15/22] ci: bump actions/setup-go from 5 to 6 Bumps actions/setup-go from 5 to 6. This upgrade includes dependency updates that incorporate a fix for a critical vulnerability. [Originally opened at https://github.com/git-for-windows/git/pull/5811] - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v5...v6) Originally-authored-by: dependabot[bot] Signed-off-by: Johannes Schindelin Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- .github/workflows/l10n.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index e2c3dbdcb50f0c..95e55134bdbed4 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -63,7 +63,7 @@ jobs: origin \ ${{ github.ref }} \ $args - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version: '>=1.16' cache: false From e96105aa1741ebb61c17a59aee962058a3743e09 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 18 Nov 2025 04:32:43 -0500 Subject: [PATCH 16/22] unit-test: ignore --no-chain-lint In the same spirit as 9faf3963b6 (t: introduce compatibility options to clar-based tests, 2024-12-13), we should ignore --no-chain-lint passed to our clar tests, since it may appear in GIT_TEST_OPTS to be used with other tests. This is particularly important on Windows CI, where --no-chain-lint is added to the test options by default, and the meson build will pass all options to the unit tests. The only reason our meson Windows CI job does not run into this currently is that it is not respecting GIT_TEST_OPTS at all! So ignoring this option is a prerequisite to fixing that situation. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- t/unit-tests/unit-test.c | 1 + 1 file changed, 1 insertion(+) diff --git a/t/unit-tests/unit-test.c b/t/unit-tests/unit-test.c index 5af645048adf4e..752fb38fb324a1 100644 --- a/t/unit-tests/unit-test.c +++ b/t/unit-tests/unit-test.c @@ -29,6 +29,7 @@ int cmd_main(int argc, const char **argv) OPT_NOOP_NOARG('d', "debug"), OPT_NOOP_NOARG(0, "github-workflow-markup"), OPT_NOOP_NOARG(0, "no-bin-wrappers"), + OPT_NOOP_ARG(0, "no-chain-lint"), OPT_NOOP_ARG(0, "root"), OPT_NOOP_ARG(0, "stress"), OPT_NOOP_NOARG(0, "tee"), From 17bd1108eac98d8977a24233a498143ffd577c31 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 18 Nov 2025 04:35:19 -0500 Subject: [PATCH 17/22] ci(windows-meson-test): handle options and output like other test jobs The GitHub windows-meson-test jobs directly run "meson test" with the --slice option. This means they skip all of the ci/lib.sh infrastructure, and in particular: 1. They do not actually set any GIT_TEST_OPTS like --verbose-log or -x. 2. They do not do the usual handle_failed_tests() magic to print test failures or tar up failed directories. As a result, you get almost no feedback at all when a test fails in this job, making debugging rather tricky. Let's try to make this behave more like the other CI jobs. Because we're on Windows, we can't just use the normal run-build-and-tests.sh script. Our build runs as a separate job (like the non-meson Windows job), and then we parallelize the tests across several job slices. So we need something like the run-test-slice.sh script that the "windows-test" job uses. In theory we could just swap out the "make" invocation there for "meson". But it doesn't quite work, because "make" knows how to pull GIT_TEST_OPTS out of GIT-BUILD-OPTIONS automatically. But for meson, we have to extract them into the --test-args option ourselves. I tried making the logic in run-test-slice.sh conditional, but there ended up being hardly any common code at all (and there are some tricky ordering constraints). So I added up with a new meson-specific test-slice runner. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- .github/workflows/main.yml | 12 +++++++++++- ci/run-test-slice-meson.sh | 13 +++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100755 ci/run-test-slice-meson.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aa6bce673b4e99..e8c824406f17f3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -298,7 +298,17 @@ jobs: path: build - name: Test shell: pwsh - run: meson test -C build --no-rebuild --print-errorlogs --slice "$(1+${{ matrix.nr }})/10" + run: ci/run-test-slice-meson.sh build ${{matrix.nr}} 10 + - name: print test failures + if: failure() && env.FAILED_TEST_ARTIFACTS != '' + shell: bash + run: ci/print-test-failures.sh + - name: Upload failed tests' directories + if: failure() && env.FAILED_TEST_ARTIFACTS != '' + uses: actions/upload-artifact@v4 + with: + name: failed-tests-windows-meson-${{ matrix.nr }} + path: ${{env.FAILED_TEST_ARTIFACTS}} regular: name: ${{matrix.vector.jobname}} (${{matrix.vector.pool}}) diff --git a/ci/run-test-slice-meson.sh b/ci/run-test-slice-meson.sh new file mode 100755 index 00000000000000..961c94fba0b2ee --- /dev/null +++ b/ci/run-test-slice-meson.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# We must load the build options so we know where to find +# things like TEST_OUTPUT_DIRECTORY. This has to come before +# loading lib.sh, though, because it may clobber some CI lib +# variables like our custom GIT_TEST_OPTS. +. "$1"/GIT-BUILD-OPTIONS +. ${0%/*}/lib.sh + +group "Run tests" \ + meson test -C "$1" --no-rebuild --print-errorlogs \ + --test-args="$GIT_TEST_OPTS" --slice "$((1+$2))/$3" || +handle_failed_tests From 14b561e7685ee91d6a3d39684f9089c902641083 Mon Sep 17 00:00:00 2001 From: Jeff King Date: Tue, 18 Nov 2025 07:21:24 -0500 Subject: [PATCH 18/22] test-mktemp: plug memory and descriptor leaks We test xmkstemp() in our helper by just calling: xmkstemp(xstrdup(argv[1])); This leaks both the copied string as well as the descriptor returned by the function. In practice this isn't a big deal, since we immediately exit the program, but: 1. LSan will complain about the memory leak. The only reason we did not notice this in our leak-checking builds is that both of the callers in the test suite (both in t0070) pass a broken template (and expect failure). So the function calls die() before we can actually leak. But it's an accident waiting to happen if anybody adds a call which succeeds. 2. Coverity complains about the descriptor leak. There's a long list of uninteresting or false positives in Coverity's results, but since we're here we might as well fix it, too. I didn't bother adding a new test that triggers the leak. It's not even in real production code, but just in the test-helper itself. Signed-off-by: Jeff King Signed-off-by: Junio C Hamano --- t/helper/test-mktemp.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/t/helper/test-mktemp.c b/t/helper/test-mktemp.c index 22906889402933..da195640a9dcc8 100644 --- a/t/helper/test-mktemp.c +++ b/t/helper/test-mktemp.c @@ -6,10 +6,16 @@ int cmd__mktemp(int argc, const char **argv) { + char *template; + int fd; + if (argc != 2) usage("Expected 1 parameter defining the temporary file template"); + template = xstrdup(argv[1]); - xmkstemp(xstrdup(argv[1])); + fd = xmkstemp(template); + close(fd); + free(template); return 0; } From a6238ee16371247517e39da36782614a229184ff Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 18 Nov 2025 16:07:32 +0000 Subject: [PATCH 19/22] worktree list: fix column spacing The output of "git worktree list" displays a table containing the worktree path, HEAD OID and branch name for each worktree. The code aligns the columns by measuring the visual width of the worktree path when it is printed. Unfortunately it fails to use the visual width when calculating the width of the column so, if any of the paths contain a multibyte character, we can end up with excess padding between columns. The simplest fix would be to replace strlen() with utf8_strwidth() in measure_widths(). However that leaves us measuring the visual width twice and the byte length once. By caching the visual width and printing the padding separately to the worktree path, we only need to calculate the visual width once and do not need the byte length at all. The visual widths are stored in an arrays of structs rather than an array of ints as the next commit will add more struct members. Even if there are no multibyte characters in any of the paths we still print an extra space between the path and the object id as the field width is calculated as one plus the length of the path and we print an explicit space as well. This is fixed by not printing the extra space. The tests are updated to include multibyte characters in one of the worktree paths and to check the spacing of the columns. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/worktree.c | 35 +++++++++++++++++++++++------------ t/t2402-worktree-list.sh | 22 ++++++++++------------ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/builtin/worktree.c b/builtin/worktree.c index 812774a5ca992c..0643a22ee58b89 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -979,14 +979,17 @@ static void show_worktree_porcelain(struct worktree *wt, int line_terminator) fputc(line_terminator, stdout); } -static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len) +struct worktree_display { + int width; +}; + +static void show_worktree(struct worktree *wt, struct worktree_display *display, + int path_maxwidth, int abbrev_len) { struct strbuf sb = STRBUF_INIT; - int cur_path_len = strlen(wt->path); - int path_adj = cur_path_len - utf8_strwidth(wt->path); const char *reason; - strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path); + strbuf_addf(&sb, "%s%*s", wt->path, 1 + path_maxwidth - display->width, ""); if (wt->is_bare) strbuf_addstr(&sb, "(bare)"); else { @@ -1020,20 +1023,24 @@ static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len) strbuf_release(&sb); } -static void measure_widths(struct worktree **wt, int *abbrev, int *maxlen) +static void measure_widths(struct worktree **wt, int *abbrev, + struct worktree_display **d, int *maxwidth) { - int i; + int i, display_alloc = 0; + struct worktree_display *display = NULL; for (i = 0; wt[i]; i++) { int sha1_len; - int path_len = strlen(wt[i]->path); + ALLOC_GROW(display, i + 1, display_alloc); + display[i].width = utf8_strwidth(wt[i]->path); - if (path_len > *maxlen) - *maxlen = path_len; + if (display[i].width > *maxwidth) + *maxwidth = display[i].width; sha1_len = strlen(repo_find_unique_abbrev(the_repository, &wt[i]->head_oid, *abbrev)); if (sha1_len > *abbrev) *abbrev = sha1_len; } + *d = display; } static int pathcmp(const void *a_, const void *b_) @@ -1079,21 +1086,25 @@ static int list(int ac, const char **av, const char *prefix, die(_("the option '%s' requires '%s'"), "-z", "--porcelain"); else { struct worktree **worktrees = get_worktrees(); - int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i; + int path_maxwidth = 0, abbrev = DEFAULT_ABBREV, i; + struct worktree_display *display = NULL; /* sort worktrees by path but keep main worktree at top */ pathsort(worktrees + 1); if (!porcelain) - measure_widths(worktrees, &abbrev, &path_maxlen); + measure_widths(worktrees, &abbrev, + &display, &path_maxwidth); for (i = 0; worktrees[i]; i++) { if (porcelain) show_worktree_porcelain(worktrees[i], line_terminator); else - show_worktree(worktrees[i], path_maxlen, abbrev); + show_worktree(worktrees[i], + &display[i], path_maxwidth, abbrev); } + free(display); free_worktrees(worktrees); } return 0; diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh index 8ef1cad7f29d7d..a494df6d612668 100755 --- a/t/t2402-worktree-list.sh +++ b/t/t2402-worktree-list.sh @@ -30,22 +30,20 @@ test_expect_success 'rev-parse --git-path objects linked worktree' ' ' test_expect_success '"list" all worktrees from main' ' - echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && - test_when_finished "rm -rf here out actual expect && git worktree prune" && - git worktree add --detach here main && - echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect && - git worktree list >out && - sed "s/ */ /g" actual && + echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && + test_when_finished "rm -rf áááá out actual expect && git worktree prune" && + git worktree add --detach áááá main && + echo "$(git -C áááá rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect && + git worktree list >actual && test_cmp expect actual ' test_expect_success '"list" all worktrees from linked' ' - echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && - test_when_finished "rm -rf here out actual expect && git worktree prune" && - git worktree add --detach here main && - echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect && - git -C here worktree list >out && - sed "s/ */ /g" actual && + echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && + test_when_finished "rm -rf áááá out actual expect && git worktree prune" && + git worktree add --detach áááá main && + echo "$(git -C áááá rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect && + git -C áááá worktree list >actual && test_cmp expect actual ' From 08dfa5983572645ae7cc51b49cadfdf216ecfec6 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Tue, 18 Nov 2025 16:07:33 +0000 Subject: [PATCH 20/22] worktree list: quote paths If a worktree path contains newlines or other control characters it messes up the output of "git worktree list". Fix this by using quote_path() to display the worktree path. The output of "git worktree list" is designed for human consumption, scripts should be using the "--porcelain" option so this change should not break them. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/worktree.c | 10 ++++++++-- t/t2402-worktree-list.sh | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/builtin/worktree.c b/builtin/worktree.c index 0643a22ee58b89..303cc3b2d64b64 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -980,6 +980,7 @@ static void show_worktree_porcelain(struct worktree *wt, int line_terminator) } struct worktree_display { + char *path; int width; }; @@ -989,7 +990,7 @@ static void show_worktree(struct worktree *wt, struct worktree_display *display, struct strbuf sb = STRBUF_INIT; const char *reason; - strbuf_addf(&sb, "%s%*s", wt->path, 1 + path_maxwidth - display->width, ""); + strbuf_addf(&sb, "%s%*s", display->path, 1 + path_maxwidth - display->width, ""); if (wt->is_bare) strbuf_addstr(&sb, "(bare)"); else { @@ -1028,11 +1029,14 @@ static void measure_widths(struct worktree **wt, int *abbrev, { int i, display_alloc = 0; struct worktree_display *display = NULL; + struct strbuf buf = STRBUF_INIT; for (i = 0; wt[i]; i++) { int sha1_len; ALLOC_GROW(display, i + 1, display_alloc); - display[i].width = utf8_strwidth(wt[i]->path); + quote_path(wt[i]->path, NULL, &buf, 0); + display[i].width = utf8_strwidth(buf.buf); + display[i].path = strbuf_detach(&buf, NULL); if (display[i].width > *maxwidth) *maxwidth = display[i].width; @@ -1104,6 +1108,8 @@ static int list(int ac, const char **av, const char *prefix, show_worktree(worktrees[i], &display[i], path_maxwidth, abbrev); } + for (i = 0; display && worktrees[i]; i++) + free(display[i].path); free(display); free_worktrees(worktrees); } diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh index a494df6d612668..e0c6abd2f58e20 100755 --- a/t/t2402-worktree-list.sh +++ b/t/t2402-worktree-list.sh @@ -29,7 +29,8 @@ test_expect_success 'rev-parse --git-path objects linked worktree' ' test_cmp expect actual ' -test_expect_success '"list" all worktrees from main' ' +test_expect_success '"list" all worktrees from main core.quotepath=false' ' + test_config core.quotepath false && echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && test_when_finished "rm -rf áááá out actual expect && git worktree prune" && git worktree add --detach áááá main && @@ -38,7 +39,19 @@ test_expect_success '"list" all worktrees from main' ' test_cmp expect actual ' +test_expect_success '"list" all worktrees from main core.quotepath=true' ' + test_config core.quotepath true && + echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && + test_when_finished "rm -rf á out actual expect && git worktree prune" && + git worktree add --detach á main && + echo "\"$(git -C á rev-parse --show-toplevel)\" $(git rev-parse --short HEAD) (detached HEAD)" | + sed s/á/\\\\303\\\\241/g >>expect && + git worktree list >actual && + test_cmp expect actual +' + test_expect_success '"list" all worktrees from linked' ' + test_config core.quotepath false && echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && test_when_finished "rm -rf áááá out actual expect && git worktree prune" && git worktree add --detach áááá main && From 2367c6bcd600882d0ea70d4f654c8cfa5c1f53ac Mon Sep 17 00:00:00 2001 From: Greg Funni Date: Tue, 18 Nov 2025 15:41:54 +0000 Subject: [PATCH 21/22] win32: return error if SleepConditionVariableCS fails If it fails, return an error. Signed-off-by: Greg Funni Signed-off-by: Junio C Hamano --- compat/win32/pthread.c | 7 +++++++ compat/win32/pthread.h | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/compat/win32/pthread.c b/compat/win32/pthread.c index 58980a529c3eb9..7e93146963ec56 100644 --- a/compat/win32/pthread.c +++ b/compat/win32/pthread.c @@ -59,3 +59,10 @@ pthread_t pthread_self(void) t.tid = GetCurrentThreadId(); return t; } + +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) +{ + if (SleepConditionVariableCS(cond, mutex, INFINITE) == 0) + return err_win_to_posix(GetLastError()); + return 0; +} diff --git a/compat/win32/pthread.h b/compat/win32/pthread.h index e2b5c4f64c9b91..859e1d9021c97c 100644 --- a/compat/win32/pthread.h +++ b/compat/win32/pthread.h @@ -36,7 +36,6 @@ typedef int pthread_mutexattr_t; #define pthread_cond_init(a,b) InitializeConditionVariable((a)) #define pthread_cond_destroy(a) do {} while (0) -#define pthread_cond_wait(a,b) return_0(SleepConditionVariableCS((a), (b), INFINITE)) #define pthread_cond_signal WakeConditionVariable #define pthread_cond_broadcast WakeAllConditionVariable @@ -64,6 +63,8 @@ int win32_pthread_join(pthread_t *thread, void **value_ptr); #define pthread_equal(t1, t2) ((t1).tid == (t2).tid) pthread_t pthread_self(void); +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); + static inline void NORETURN pthread_exit(void *ret) { _endthreadex((unsigned)(uintptr_t)ret); From b31ab939fe8e3cbe8be48dddd1c6ac0265991f45 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 26 Nov 2025 10:22:08 -0800 Subject: [PATCH 22/22] The fourth batch Signed-off-by: Junio C Hamano --- Documentation/RelNotes/2.53.0.adoc | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Documentation/RelNotes/2.53.0.adoc b/Documentation/RelNotes/2.53.0.adoc index 7882bc59e80a92..70c4338675ae52 100644 --- a/Documentation/RelNotes/2.53.0.adoc +++ b/Documentation/RelNotes/2.53.0.adoc @@ -11,6 +11,8 @@ UI, Workflows & Features in a transaction by default, instead of emitting where each refs should point at and leaving the actual update to another command. + * "git blame" learns "--diff-algorithm=" option. + Performance, Internal Implementation, Development Support etc. -------------------------------------------------------------- @@ -57,6 +59,43 @@ Fixes since v2.52 corrected. (merge 7a03a10a3a jx/repo-struct-utf8width-fix later to maint). + * Yet another corner case fix around renames in the "ort" merge + strategy. + (merge a562d90a35 en/ort-rename-another-fix later to maint). + + * Test leakfix. + (merge 14b561e768 jk/test-mktemp-leakfix later to maint). + + * Update a version of action used at the GitHub Actrions CI. + (merge cd99203f86 js/ci-github-setup-go-update later to maint). + + * The "return errno = EFOO, -1" construct, which is heavily used in + compat/mingw.c and triggers warnings under "-Wcomma", has been + rewritten to avoid the warnings. + (merge af3919816f js/mingw-assign-comma-fix later to maint). + + * Makefile based build have recently been updated to build a + libgit.a that also has reftable and xdiff objects; CMake based + build procedure has been updated to match. + (merge b0d5c88cca js/cmake-libgit-fix later to maint). + + * Under-allocation fix. + (merge d22a488482 js/wincred-get-credential-alloc-fix later to maint). + + * "git worktree list" attempts to show paths to worktrees while + aligning them, but miscounted display columns for the paths when + non-ASCII characters were involved, which has been corrected. + (merge 08dfa59835 pw/worktree-list-display-width-fix later to maint). + + * "Windows+meson" job at the GitHub Actions CI was hard to debug, as + it did not show and save failed test artifacts, which has been + corrected. + (merge 17bd1108ea jk/ci-windows-meson-test-fix later to maint). + + * Emulation code clean-up. + (merge 2367c6bcd6 gf/win32-pthread-cond-wait-err later to maint). + * Other code cleanup, docfix, build fix, etc. (merge 46207a54cc qj/doc-http-bad-want-response later to maint). (merge df90eccd93 kh/doc-commit-extra-references later to maint). + (merge f18aa68861 rs/xmkstemp-simplify later to maint).