{"thread":{"id":"63732","subject":"Behavior of git log --diff-filter=d","startedAt":"2025-07-02T20:28:46Z","lastAt":"2025-07-07T05:31:59Z","messageCount":9,"participants":["Eric Salem","K Jayatheerth","Jeff King","Karthik Nayak","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"521203","messageId":"64308d3f-281b-49a3-bc10-57878903bf4c@gmail.com","threadId":"63732","inReplyTo":null,"subject":"Behavior of git log --diff-filter=d","fromName":"Eric Salem","fromEmail":"ericsalem@gmail.com","sentAt":"2025-07-02T20:28:43Z","receivedAt":"2025-07-02T20:28:46Z","isPatch":false,"sender":{"key":"ericsalem@gmail.com","avatar":null},"body":"Hi all. What am I doing wrong here?\n\nThe git log --diff-filter documentation[1] for deleted files says:\n\n> Select only files that are Added (A), Copied (C), Deleted (D)...\n\n> Also, these upper-case letters can be downcased to exclude.\n> E.g. --diff-filter=ad excludes added and deleted paths.\n\nA simple test:\n\n$ cd $(mktemp -d)\n$ git init\nInitialized empty Git repository in /tmp/tmp.xnvNav956B/.git/\n$ echo test1 > file1.txt\n$ git add file1.txt \n$ git commit -m \"first\"\n[master (root-commit) 71288dd00aff] first\n 1 file changed, 1 insertion(+)\n create mode 100644 file1.txt\n$ git rm file1.txt \nrm 'file1.txt'\n$ git commit -m \"second\"\n[master 6ff8f522b744] second\n 1 file changed, 1 deletion(-)\n delete mode 100644 file1.txt\n$ echo test2 > file2.txt\n$ git add file2.txt \n$ git commit -m \"third\"\n[master 41498d26ea5e] third\n 1 file changed, 1 insertion(+)\n create mode 100644 file2.txt\n$ git log --format=\"%H\"\n41498d26ea5ee6820834aa51351e1e7ce6ce8733\n6ff8f522b744dfbc7c2201c5bf77cf5acc3028ce\n71288dd00afff60f3a6576f93930aeb0130e5cd1\n$ git log --format=\"%H\" --diff-filter=D\n6ff8f522b744dfbc7c2201c5bf77cf5acc3028ce\n$ git log --format=\"%H\" --diff-filter=d\n$ git log --format=\"%H\" --diff-filter=d --stat\n41498d26ea5ee6820834aa51351e1e7ce6ce8733\n\n file2.txt | 1 +\n 1 file changed, 1 insertion(+)\n71288dd00afff60f3a6576f93930aeb0130e5cd1\n\n file1.txt | 1 +\n 1 file changed, 1 insertion(+)\n$ git version\ngit version 2.50.0\n\n--diff-filter=D behaves as expected, but when using \"d\" instead, I don't\nget any output unless I add another option (such as --stat or\n--name-only).\n\nIs this expected behavior?\n\nThanks,\n\nEric\n\n[1] https://git-scm.com/docs/git-log#Documentation/git-log.txt---diff-filterACDMRTUXB\n"},{"id":"521275","messageId":"20250703134220.31638-1-jayatheerthkulkarni2005@gmail.com","threadId":"63732","inReplyTo":"64308d3f-281b-49a3-bc10-57878903bf4c@gmail.com","subject":"Re Behavior of git log --diff-filter=d","fromName":"K Jayatheerth","fromEmail":"jayatheerthkulkarni2005@gmail.com","sentAt":"2025-07-03T13:42:20Z","receivedAt":"2025-07-03T13:42:30Z","isPatch":false,"sender":{"key":"jayatheerthkulkarni2005@gmail.com","avatar":"https://avatars.githubusercontent.com/u/148841023?v=4"},"body":"Hi Eric,\n\nNice question\n\nYes, this is expected behavior\nYou're seeing a difference because\n\n--diff-filter=d tells Git to exclude commits that have deleted files in their diffs.\nHowever, this filter only applies if there is a diff to filter.\n\nIn other words, \nif you run git log --format=\"%H\" --diff-filter=d, \nGit doesn't show any output unless \nthe diff logic is actually invoked. \nBut --format=\"%H\" alone does not invoke diff generation...\nso --diff-filter silently does nothing.\n\nWhen you add --stat or --name-only, \nyou're explicitly telling Git: \n\"Please compute the diff\". \n\nNow Git has something to filter, \nand it applies --diff-filter=d \nto exclude those diffs that involve deletions.\n\nIf you want to exclude deletion commits and get just commit hashes\nThis is something I think should work\nprobably set this up with an alias if you use this many times\n\ngit log --format=\"%H\" --diff-filter=d --name-only | grep -v '^$'\n\nThis should work just fine...\n\nOr perhaps if you wanna tinker more\ngit log --format=\"%H\" --diff-filter=d --stat | grep -B1 -v \"delete mode\"\n\nBut if you're just trying to filter commits \nby file change type and want to see \nonly those hashes where a deleted file is not present, \nyou'll need some way to trigger the diff \neven if you discard the output later.\n\nHope that helps :)\n\nThank you,\n\n- Jayatheerth\n"},{"id":"521279","messageId":"20250703153611.GB1309870@coredump.intra.peff.net","threadId":"63732","inReplyTo":"20250703134220.31638-1-jayatheerthkulkarni2005@gmail.com","subject":"Re: Re Behavior of git log --diff-filter=d","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-07-03T15:36:11Z","receivedAt":"2025-07-03T15:36:12Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Jul 03, 2025 at 07:12:20PM +0530, K Jayatheerth wrote:\n\n> Yes, this is expected behavior\n> You're seeing a difference because\n> \n> --diff-filter=d tells Git to exclude commits that have deleted files in their diffs.\n> However, this filter only applies if there is a diff to filter.\n\nI don't think that is true. Even if we are not showing a diff, we'll\nselect which commits to show based on diff options (and show only those\nthat still touch something). That's why \"--diff-filter=D\" shows only the\ncommit with the deletion. With the \"d\" filter we should omit deletions,\nleaving the second commit with no changes (and thus not shown).\n\n-Peff\n"},{"id":"521282","messageId":"20250703153438.GA1309870@coredump.intra.peff.net","threadId":"63732","inReplyTo":"64308d3f-281b-49a3-bc10-57878903bf4c@gmail.com","subject":"Re: Behavior of git log --diff-filter=d","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-07-03T15:34:38Z","receivedAt":"2025-07-03T15:41:23Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Jul 02, 2025 at 03:28:43PM -0500, Eric Salem wrote:\n\n> The git log --diff-filter documentation[1] for deleted files says:\n> \n> > Select only files that are Added (A), Copied (C), Deleted (D)...\n> \n> > Also, these upper-case letters can be downcased to exclude.\n> > E.g. --diff-filter=ad excludes added and deleted paths.\n> \n> A simple test:\n> [...]\n> --diff-filter=D behaves as expected, but when using \"d\" instead, I don't\n> get any output unless I add another option (such as --stat or\n> --name-only).\n\nLooks like a bug. This used to produce the output I'd expect (i.e.,\ncommits \"first\" and \"third\", which do not have deletions), but that\nchanged in 75408ca949 (diff-filter: be more careful when looking for\nnegative bits, 2022-01-28).\n\nI don't have time to dig into it now, but I've cc'd the author (and left\nyour whole reproduction recipe quoted below).\n\n-Peff\n\n> $ cd $(mktemp -d)\n> $ git init\n> Initialized empty Git repository in /tmp/tmp.xnvNav956B/.git/\n> $ echo test1 > file1.txt\n> $ git add file1.txt \n> $ git commit -m \"first\"\n> [master (root-commit) 71288dd00aff] first\n>  1 file changed, 1 insertion(+)\n>  create mode 100644 file1.txt\n> $ git rm file1.txt \n> rm 'file1.txt'\n> $ git commit -m \"second\"\n> [master 6ff8f522b744] second\n>  1 file changed, 1 deletion(-)\n>  delete mode 100644 file1.txt\n> $ echo test2 > file2.txt\n> $ git add file2.txt \n> $ git commit -m \"third\"\n> [master 41498d26ea5e] third\n>  1 file changed, 1 insertion(+)\n>  create mode 100644 file2.txt\n> $ git log --format=\"%H\"\n> 41498d26ea5ee6820834aa51351e1e7ce6ce8733\n> 6ff8f522b744dfbc7c2201c5bf77cf5acc3028ce\n> 71288dd00afff60f3a6576f93930aeb0130e5cd1\n> $ git log --format=\"%H\" --diff-filter=D\n> 6ff8f522b744dfbc7c2201c5bf77cf5acc3028ce\n> $ git log --format=\"%H\" --diff-filter=d\n> $ git log --format=\"%H\" --diff-filter=d --stat\n> 41498d26ea5ee6820834aa51351e1e7ce6ce8733\n> \n>  file2.txt | 1 +\n>  1 file changed, 1 insertion(+)\n> 71288dd00afff60f3a6576f93930aeb0130e5cd1\n> \n>  file1.txt | 1 +\n>  1 file changed, 1 insertion(+)\n> $ git version\n> git version 2.50.0\n"},{"id":"521288","messageId":"dea87c81-c2a4-4dad-b73b-bdd11d4c6877@gmail.com","threadId":"63732","inReplyTo":"20250703134220.31638-1-jayatheerthkulkarni2005@gmail.com","subject":"Re: Re Behavior of git log --diff-filter=d","fromName":"Eric Salem","fromEmail":"ericsalem@gmail.com","sentAt":"2025-07-03T21:30:11Z","receivedAt":"2025-07-03T21:30:13Z","isPatch":false,"sender":{"key":"ericsalem@gmail.com","avatar":null},"body":"Thank you, Jayatheerth. That was very helpful. Now it makes sense why\nGit is behaving like this. If I use --dirstat=<param> and then filter\nout the blank lines, I get the output I want.\n\nThanks again for your help.\n\nEric\n"},{"id":"521289","messageId":"20250703224428.GB1909836@coredump.intra.peff.net","threadId":"63732","inReplyTo":"20250703153438.GA1309870@coredump.intra.peff.net","subject":"[PATCH] setup_revisions(): turn on diffs for all-negative diff filter","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-07-03T22:44:28Z","receivedAt":"2025-07-03T22:44:30Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Thu, Jul 03, 2025 at 11:34:38AM -0400, Jeff King wrote:\n\n> On Wed, Jul 02, 2025 at 03:28:43PM -0500, Eric Salem wrote:\n> \n> > The git log --diff-filter documentation[1] for deleted files says:\n> > \n> > > Select only files that are Added (A), Copied (C), Deleted (D)...\n> > \n> > > Also, these upper-case letters can be downcased to exclude.\n> > > E.g. --diff-filter=ad excludes added and deleted paths.\n> > \n> > A simple test:\n> > [...]\n> > --diff-filter=D behaves as expected, but when using \"d\" instead, I don't\n> > get any output unless I add another option (such as --stat or\n> > --name-only).\n> \n> Looks like a bug. This used to produce the output I'd expect (i.e.,\n> commits \"first\" and \"third\", which do not have deletions), but that\n> changed in 75408ca949 (diff-filter: be more careful when looking for\n> negative bits, 2022-01-28).\n> \n> I don't have time to dig into it now, but I've cc'd the author (and left\n> your whole reproduction recipe quoted below).\n\nArgh, I forgot to add Johannes to the cc. Fortunately since then I had a\nmoment to look at this, and the solution is pretty simple. So here it is\nas a patch with a test.\n\n-- >8 --\nSubject: setup_revisions(): turn on diffs for all-negative diff filter\n\nWhen the user gives us a diff filter like --diff-filter=D, we need to do\na tree diff even if we're not planning to show the diff result itself,\nin order to decide whether to show the commit at all. So there's an\nexplicit check of revs->diffopt.filter in setup_revisions(), and we set\nrevs->diff if any bits are set.\n\nOriginally that \"filter\" field covered both positive capital-letter\nfilters (like \"D\") and also negative lowercase filters (like \"d\"), so it\nwas sufficient for both cases. But later, 75408ca949 (diff-filter: be\nmore careful when looking for negative bits, 2022-01-28) split the\nnegative bits out into a \"filter_not\" field.\n\nWe eventually fold those into \"filter\", but not until diff_setup_done()\nis called, which happens after our explicit check. As a result, a purely\nnegative filter like:\n\n  git log --diff-filter=d\n\nfailed to turn on diffs at all. But rather than fail to filter by diff,\nbecause the filter variable is eventually set, we mistakenly show no\ncommits at all, thinking that the empty diffs were cases where nothing\npassed through the filter.\n\nThe smallest fix here is to just have our check look for any bits in\neither \"filter\" or \"filter_not\". I suspect it would also be OK to\nreorder the function a bit to call diff_setup_done() earlier, but that\nrisks violating some other subtle ordering dependency. So I went with\nthe simple and safe solution here.\n\nSigned-off-by: Jeff King <peff@peff.net>\n---\n revision.c     | 2 +-\n t/t4202-log.sh | 6 ++++++\n 2 files changed, 7 insertions(+), 1 deletion(-)\n\ndiff --git a/revision.c b/revision.c\nindex afee111196..9892d08748 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -3112,7 +3112,7 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \n \t/* Pickaxe, diff-filter and rename following need diffs */\n \tif ((revs->diffopt.pickaxe_opts & DIFF_PICKAXE_KINDS_MASK) ||\n-\t    revs->diffopt.filter ||\n+\t    revs->diffopt.filter || revs->diffopt.filter_not ||\n \t    revs->diffopt.flags.follow_renames)\n \t\trevs->diff = 1;\n \ndiff --git a/t/t4202-log.sh b/t/t4202-log.sh\nindex 4a6c4dfbf4..05cee9e41b 100755\n--- a/t/t4202-log.sh\n+++ b/t/t4202-log.sh\n@@ -134,6 +134,12 @@ test_expect_success 'diff-filter=D' '\n \n '\n \n+test_expect_success 'all-negative filter' '\n+\tgit log --no-renames --format=%s --diff-filter=d HEAD >actual &&\n+\tprintf \"%s\\n\" fifth fourth third second initial >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'diff-filter=R' '\n \n \tgit log -M --pretty=\"format:%s\" --diff-filter=R HEAD >actual &&\n-- \n2.50.0.438.g3b3bebd3e8\n\n\n"},{"id":"521290","messageId":"cb8eb906-b0eb-43fe-b7c9-3fbf8c84f4d1@gmail.com","threadId":"63732","inReplyTo":"20250703224428.GB1909836@coredump.intra.peff.net","subject":"Re: [PATCH] setup_revisions(): turn on diffs for all-negative diff filter","fromName":"Eric Salem","fromEmail":"ericsalem@gmail.com","sentAt":"2025-07-03T23:42:37Z","receivedAt":"2025-07-03T23:42:41Z","isPatch":true,"sender":{"key":"ericsalem@gmail.com","avatar":null},"body":"On 7/3/25 5:44 PM, Jeff King wrote:\n> Argh, I forgot to add Johannes to the cc. Fortunately since then I had a\n> moment to look at this, and the solution is pretty simple. So here it is\n> as a patch with a test.\n\nPatch worked great for me. Thanks for fixing it so quickly, Jeff!\n\nEric\n"},{"id":"521318","messageId":"CAOLa=ZRKMDKOFCLvcqyWKY_P7ruZSZPTfStXMnDt_eGOtY41OA@mail.gmail.com","threadId":"63732","inReplyTo":"20250703224428.GB1909836@coredump.intra.peff.net","subject":"Re: [PATCH] setup_revisions(): turn on diffs for all-negative diff filter","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-07-04T13:30:15Z","receivedAt":"2025-07-04T13:30:18Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> On Thu, Jul 03, 2025 at 11:34:38AM -0400, Jeff King wrote:\n>\n>> On Wed, Jul 02, 2025 at 03:28:43PM -0500, Eric Salem wrote:\n>>\n>> > The git log --diff-filter documentation[1] for deleted files says:\n>> >\n>> > > Select only files that are Added (A), Copied (C), Deleted (D)...\n>> >\n>> > > Also, these upper-case letters can be downcased to exclude.\n>> > > E.g. --diff-filter=ad excludes added and deleted paths.\n>> >\n>> > A simple test:\n>> > [...]\n>> > --diff-filter=D behaves as expected, but when using \"d\" instead, I don't\n>> > get any output unless I add another option (such as --stat or\n>> > --name-only).\n>>\n>> Looks like a bug. This used to produce the output I'd expect (i.e.,\n>> commits \"first\" and \"third\", which do not have deletions), but that\n>> changed in 75408ca949 (diff-filter: be more careful when looking for\n>> negative bits, 2022-01-28).\n>>\n>> I don't have time to dig into it now, but I've cc'd the author (and left\n>> your whole reproduction recipe quoted below).\n>\n> Argh, I forgot to add Johannes to the cc. Fortunately since then I had a\n> moment to look at this, and the solution is pretty simple. So here it is\n> as a patch with a test.\n>\n> -- >8 --\n> Subject: setup_revisions(): turn on diffs for all-negative diff filter\n>\n> When the user gives us a diff filter like --diff-filter=D, we need to do\n> a tree diff even if we're not planning to show the diff result itself,\n> in order to decide whether to show the commit at all. So there's an\n> explicit check of revs->diffopt.filter in setup_revisions(), and we set\n> revs->diff if any bits are set.\n\nSo if `revs->diff` is set, then we compute the tree diff for the given\ncommit.\n\n>\n> Originally that \"filter\" field covered both positive capital-letter\n> filters (like \"D\") and also negative lowercase filters (like \"d\"), so it\n> was sufficient for both cases. But later, 75408ca949 (diff-filter: be\n> more careful when looking for negative bits, 2022-01-28) split the\n> negative bits out into a \"filter_not\" field.\n>\n> We eventually fold those into \"filter\", but not until diff_setup_done()\n> is called, which happens after our explicit check. As a result, a purely\n> negative filter like:\n>\n>   git log --diff-filter=d\n>\n> failed to turn on diffs at all. But rather than fail to filter by diff,\n> because the filter variable is eventually set, we mistakenly show no\n> commits at all, thinking that the empty diffs were cases where nothing\n> passed through the filter.\n>\n> The smallest fix here is to just have our check look for any bits in\n> either \"filter\" or \"filter_not\". I suspect it would also be OK to\n> reorder the function a bit to call diff_setup_done() earlier, but that\n> risks violating some other subtle ordering dependency. So I went with\n> the simple and safe solution here.\n>\n\nThe explanation here was really nice to read and explained the problem\nwell.\n\n> Signed-off-by: Jeff King <peff@peff.net>\n> ---\n>  revision.c     | 2 +-\n>  t/t4202-log.sh | 6 ++++++\n>  2 files changed, 7 insertions(+), 1 deletion(-)\n>\n> diff --git a/revision.c b/revision.c\n> index afee111196..9892d08748 100644\n> --- a/revision.c\n> +++ b/revision.c\n> @@ -3112,7 +3112,7 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n>\n>  \t/* Pickaxe, diff-filter and rename following need diffs */\n>  \tif ((revs->diffopt.pickaxe_opts & DIFF_PICKAXE_KINDS_MASK) ||\n> -\t    revs->diffopt.filter ||\n> +\t    revs->diffopt.filter || revs->diffopt.filter_not ||\n>  \t    revs->diffopt.flags.follow_renames)\n>  \t\trevs->diff = 1;\n>\n\nMakes sense.\n\n> diff --git a/t/t4202-log.sh b/t/t4202-log.sh\n> index 4a6c4dfbf4..05cee9e41b 100755\n> --- a/t/t4202-log.sh\n> +++ b/t/t4202-log.sh\n> @@ -134,6 +134,12 @@ test_expect_success 'diff-filter=D' '\n>\n>  '\n>\n> +test_expect_success 'all-negative filter' '\n> +\tgit log --no-renames --format=%s --diff-filter=d HEAD >actual &&\n> +\tprintf \"%s\\n\" fifth fourth third second initial >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n>  test_expect_success 'diff-filter=R' '\n>\n>  \tgit log -M --pretty=\"format:%s\" --diff-filter=R HEAD >actual &&\n> --\n> 2.50.0.438.g3b3bebd3e8\n\nThe fix looks great. Thanks!\n"},{"id":"521408","messageId":"xmqq7c0ka7n6.fsf@gitster.g","threadId":"63732","inReplyTo":"20250703224428.GB1909836@coredump.intra.peff.net","subject":"Re: [PATCH] setup_revisions(): turn on diffs for all-negative diff filter","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-07T05:31:57Z","receivedAt":"2025-07-07T05:31:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jeff King <peff@peff.net> writes:\n\n> Argh, I forgot to add Johannes to the cc. Fortunately since then I had a\n> moment to look at this, and the solution is pretty simple. So here it is\n> as a patch with a test.\n>\n> -- >8 --\n> Subject: setup_revisions(): turn on diffs for all-negative diff filter\n>\n> When the user gives us a diff filter like --diff-filter=D, we need to do\n> a tree diff even if we're not planning to show the diff result itself,\n> in order to decide whether to show the commit at all. So there's an\n> explicit check of revs->diffopt.filter in setup_revisions(), and we set\n> revs->diff if any bits are set.\n>\n> Originally that \"filter\" field covered both positive capital-letter\n> filters (like \"D\") and also negative lowercase filters (like \"d\"), so it\n> was sufficient for both cases. But later, 75408ca949 (diff-filter: be\n> more careful when looking for negative bits, 2022-01-28) split the\n> negative bits out into a \"filter_not\" field.\n\nAh, ouch, so the patch somehow ended up to be less careful about\nnegative bits after all ;-)\n\nThanks for noticing the breakage and fixing it.\n"}]}