{"thread":{"id":"65592","subject":"[PATCH] log: let --follow follow renames in merge commits","startedAt":"2026-05-05T07:42:18Z","lastAt":"2026-05-05T07:42:18Z","messageCount":1,"participants":["Miklos Vajna"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"542751","messageId":"afmfSa-p-9vuDL3E@collabora.com","threadId":"65592","inReplyTo":null,"subject":"[PATCH] log: let --follow follow renames in merge commits","fromName":"Miklos Vajna","fromEmail":"vmiklos@collabora.com","sentAt":"2026-05-05T07:42:01Z","receivedAt":"2026-05-05T07:42:18Z","isPatch":true,"body":"Have a repo with a subtree merge, do a 'git log --follow prefix/test.c',\nthe output only contains history in the outer repo, not commits that\nwere merged via a subtree merge.\n\nThis is inconsistent, since doing a 'git blame prefix/test.c' does find\nthe original commits. This works because find_rename() in blame.c is\ninvoked for each parent, and there diff_tree_oid() is used, which uses\ntry_to_follow_renames(). This means that in case a rename happens as\npart of a merge commit, git blame can follow that rename.\n\nFix the problem in a similar way for the 'git log --follow' case: in\ncase log_tree_diff() finds a merge commit and it would return early,\nthen do some extra work in the follow_renames case first. Check each\nparent, use diff_tree_oid() and if found_follow is set, then work with\nthat parent instead of returning.\n\nThis means that users examining the history of a repo with subtree\nmerges can see all commits to a file with a single 'git log --follow'\ninvocation, instead of one invocation for the outer repo and one for the\nhistory before the subtree merge.\n\nSigned-off-by: Miklos Vajna <vmiklos@collabora.com>\n---\n\nHi,\n\nI recently ran into a case where a subtree merge was used to do a\none-off import of one repo into an other one, into a subdirectory. Turns\nout git blame finds original commits nicely, but git log --follow is\nless great for this case. This is an improvement to also follow renames\nwhen the rename happens as part of a merge commit.\n\nLet me know if I managed to get some formality wrong, I haven't\ncontributed since 2022.\n\nCould you please review this?\n\nThanks,\n\nMiklos\n\n log-tree.c                          | 20 ++++++++++++++++-\n t/meson.build                       |  1 +\n t/t4218-log-follow-subtree-merge.sh | 34 +++++++++++++++++++++++++++++\n 3 files changed, 54 insertions(+), 1 deletion(-)\n create mode 100755 t/t4218-log-follow-subtree-merge.sh\n\ndiff --git a/log-tree.c b/log-tree.c\nindex 7e048701d0..bce09c7dac 100644\n--- a/log-tree.c\n+++ b/log-tree.c\n@@ -1142,8 +1142,26 @@ static int log_tree_diff(struct rev_info *opt, struct commit *commit, struct log\n \t\t\t\t/* Show parent info for multiple diffs */\n \t\t\t\tlog->parent = parents->item;\n \t\t\t}\n-\t\t} else\n+\t\t} else {\n+\t\t\tif (opt->diffopt.flags.follow_renames) {\n+\t\t\t\t/*\n+\t\t\t\t * Detect a rename across one of the parents.\n+\t\t\t\t * Check each parent till we find a follow.\n+\t\t\t\t */\n+\t\t\t\tstruct commit_list *p;\n+\t\t\t\tfor (p = parents; p; p = p->next) {\n+\t\t\t\t\tparse_commit_or_die(p->item);\n+\t\t\t\t\tdiff_tree_oid(get_commit_tree_oid(p->item),\n+\t\t\t\t\t\t      oid, \"\", &opt->diffopt);\n+\t\t\t\t\tdiff_queue_clear(&diff_queued_diff);\n+\t\t\t\t\tif (opt->diffopt.found_follow) {\n+\t\t\t\t\t\topt->diffopt.found_follow = 0;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n \t\t\treturn 0;\n+\t\t}\n \t}\n \n \tshowed_log = 0;\ndiff --git a/t/meson.build b/t/meson.build\nindex 7528e5cda5..b4ae8d76d8 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -574,6 +574,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-follow-subtree-merge.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\n   't4254-am-corrupt.sh',\ndiff --git a/t/t4218-log-follow-subtree-merge.sh b/t/t4218-log-follow-subtree-merge.sh\nnew file mode 100755\nindex 0000000000..7ca607cbb8\n--- /dev/null\n+++ b/t/t4218-log-follow-subtree-merge.sh\n@@ -0,0 +1,34 @@\n+#!/bin/sh\n+\n+test_description='Test --follow follows renames across subtree merges'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup subtree-merged repository' '\n+\tgit init inner &&\n+\techo inner >inner/inner.txt &&\n+\tgit -C inner add inner.txt &&\n+\tgit -C inner commit -m \"inner init\" &&\n+\n+\tgit init outer &&\n+\techo outer >outer/outer.txt &&\n+\tgit -C outer add outer.txt &&\n+\tgit -C outer commit -m \"outer init\" &&\n+\n+\tgit -C outer fetch ../inner master &&\n+\tgit -C outer merge -s ours --no-commit --allow-unrelated-histories \\\n+\t\tFETCH_HEAD &&\n+\tgit -C outer read-tree --prefix=inner/ -u FETCH_HEAD &&\n+\tgit -C outer commit -m \"Merge inner repo into inner/ subdirectory\"\n+'\n+\n+test_expect_success '--follow finds the pre-merge commit through a subtree merge' '\n+\tgit -C outer log --follow --pretty=tformat:%s inner/inner.txt >actual &&\n+\techo \"inner init\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.51.0\n\n"}]}