line-log: fix crash when combined with pickaxe options

queue_diffs() passes the caller's diff_options, which may carry user-specified pickaxe state, to diff_tree_oid() and diffcore_std() when detecting renames for line-level history tracking. When pickaxe options are present on the command line (-G and -S to filter by text pattern, --find-object to filter by object identity), diffcore_std() also runs diffcore_pickaxe(), which may discard diff pairs that are relevant for rename detection. Losing those pairs breaks rename following. Before a2bb801f6a (line-log: avoid unnecessary full tree diffs, 2019-08-21), this silently truncated history at rename boundaries. That commit moved filter_diffs_for_paths() inside the rename- detection block, so it only runs when diff_might_be_rename() returns true. When pickaxe discards a rename pair, the rename goes undetected, and a deletion pair at a subsequent commit passes through uncleaned, reaching process_diff_filepair() with an invalid filespec and triggering an assertion failure. Fix this by building a private diff_options for the rename-detection path inside queue_diffs(), following the same pattern used by blame's find_rename(). This isolates the rename machinery from unrelated user-specified options. Reported-by: Matthew Hughes <matthewhughes934@gmail.com> Signed-off-by: Michael Montalbo <mmontalbo@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michael Montalbo committed Mar 17, 2026 at 02:21 UTC 81cf6ccc29002467f44798ada7d74993a44c94b0
2 files changed +73 -4
line-log.c
+18 -4
@@ -858,15 +858,29 @@ static void queue_diffs(struct line_log_data *range,
858 diff_queue_clear(&diff_queued_diff);
859 diff_tree_oid(parent_tree_oid, tree_oid, "", opt);
860 if (opt->detect_rename && diff_might_be_rename()) {
861 + struct diff_options rename_opts;
862 +
863 + /*
864 + * Build a private diff_options for rename detection so
865 + * that any user-specified options on the original opts
866 + * (e.g. pickaxe) cannot discard diff pairs needed for
867 + * rename tracking. Similar to blame's find_rename().
868 + */
869 + repo_diff_setup(opt->repo, &rename_opts);
870 + rename_opts.flags.recursive = 1;
871 + rename_opts.detect_rename = opt->detect_rename;
872 + rename_opts.rename_score = opt->rename_score;
873 + rename_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
874 + diff_setup_done(&rename_opts);
875 +
876 /* must look at the full tree diff to detect renames */
862 - clear_pathspec(&opt->pathspec);
877 diff_queue_clear(&diff_queued_diff);
864 -
865 - diff_tree_oid(parent_tree_oid, tree_oid, "", opt);
878 + diff_tree_oid(parent_tree_oid, tree_oid, "", &rename_opts);
879
880 filter_diffs_for_paths(range, 1);
868 - diffcore_std(opt);
881 + diffcore_std(&rename_opts);
882 filter_diffs_for_paths(range, 0);
883 + diff_free(&rename_opts);
884 }
885 move_diff_queue(queue, &diff_queued_diff);
886 }
t/t4211-line-log.sh
+55
@@ -367,4 +367,59 @@ test_expect_success 'show line-log with graph' '
367 test_cmp expect actual
368 '
369
370 +test_expect_success 'setup for -L with -G/-S/--find-object and a merge with rename' '
371 + git checkout --orphan pickaxe-rename &&
372 + git reset --hard &&
373 +
374 + echo content >file &&
375 + git add file &&
376 + git commit -m "add file" &&
377 +
378 + git checkout -b pickaxe-rename-side &&
379 + git mv file renamed-file &&
380 + git commit -m "rename file" &&
381 +
382 + git checkout pickaxe-rename &&
383 + git commit --allow-empty -m "diverge" &&
384 + git merge --no-edit pickaxe-rename-side &&
385 +
386 + git mv renamed-file file &&
387 + git commit -m "rename back"
388 +'
389 +
390 +test_expect_success '-L -G does not crash with merge and rename' '
391 + git log --format="%s" --no-patch -L 1,1:file -G "." >actual
392 +'
393 +
394 +test_expect_success '-L -S does not crash with merge and rename' '
395 + git log --format="%s" --no-patch -L 1,1:file -S content >actual
396 +'
397 +
398 +test_expect_success '-L --find-object does not crash with merge and rename' '
399 + git log --format="%s" --no-patch -L 1,1:file \
400 + --find-object=$(git rev-parse HEAD:file) >actual
401 +'
402 +
403 +# Commit-level filtering with pickaxe does not yet work for -L.
404 +# show_log() prints the commit header before diffcore_std() runs
405 +# pickaxe, so commits cannot be suppressed even when no diff pairs
406 +# survive filtering. Fixing this would require deferring show_log()
407 +# until after diffcore_std(), which is a larger restructuring of the
408 +# log-tree output pipeline.
409 +test_expect_failure '-L -G should filter commits by pattern' '
410 + git log --format="%s" --no-patch -L 1,1:file -G "nomatch" >actual &&
411 + test_must_be_empty actual
412 +'
413 +
414 +test_expect_failure '-L -S should filter commits by pattern' '
415 + git log --format="%s" --no-patch -L 1,1:file -S "nomatch" >actual &&
416 + test_must_be_empty actual
417 +'
418 +
419 +test_expect_failure '-L --find-object should filter commits by object' '
420 + git log --format="%s" --no-patch -L 1,1:file \
421 + --find-object=$ZERO_OID >actual &&
422 + test_must_be_empty actual
423 +'
424 +
425 test_done