{"thread":{"id":"66480","subject":"[PATCH] combine-diff: honor --relative when printing paths","startedAt":"2026-10-07T05:17:34Z","lastAt":"2026-10-07T17:53:05Z","messageCount":6,"participants":["Muhammed Dilshad A","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"554349","messageId":"20261007051734.62590-1-dilsheddilu123@gmail.com","threadId":"66480","inReplyTo":null,"subject":"[PATCH] combine-diff: honor --relative when printing paths","fromName":"Muhammed Dilshad A","fromEmail":"dilsheddilu123@gmail.com","sentAt":"2026-10-07T05:17:34Z","receivedAt":"2026-10-07T05:17:34Z","isPatch":true,"sender":{"key":"dilsheddilu123@gmail.com","avatar":null},"body":"During a merge conflict, \"git diff --relative\" in a subdirectory still\nprints paths from the repository root. Combined raw output does the same.\n\nStrip the requested prefix when printing these paths. Keep the original\npaths for reading files from the working tree.\n\nMake the known failure in t4045 a passing test, and cover explicit\nprefixes, --no-relative, raw and NUL-separated output, quoted filenames\nand renamed parent paths.\n\nSigned-off-by: Muhammed Dilshad A <dilsheddilu123@gmail.com>\n---\n combine-diff.c           | 19 +++++++++---\n t/t4038-diff-combined.sh | 22 ++++++++++++++\n t/t4045-diff-relative.sh | 62 +++++++++++++++++++++++++++++++++++++++-\n 3 files changed, 98 insertions(+), 5 deletions(-)\n\ndiff --git a/combine-diff.c b/combine-diff.c\nindex 717d537688..78d2852cce 100644\n--- a/combine-diff.c\n+++ b/combine-diff.c\n@@ -902,6 +902,14 @@ static void reuse_combine_diff(struct sline *sline, unsigned long cnt,\n \tsline->p_lno[i] = sline->p_lno[j];\n }\n \n+static const char *strip_relative_prefix(const struct diff_options *opt,\n+\t\t\t\t\tconst char *path)\n+{\n+\tif (opt->prefix && skip_prefix(path, opt->prefix, &path) && *path == '/')\n+\t\tpath++;\n+\treturn path;\n+}\n+\n static void dump_quoted_path(const char *head,\n \t\t\t     const char *prefix,\n \t\t\t     const char *path,\n@@ -932,6 +940,7 @@ static void show_combined_header(struct combine_diff_path *elem,\n \tconst char *b_prefix = opt->b_prefix ? opt->b_prefix : \"b/\";\n \tconst char *c_meta = diff_get_color_opt(opt, DIFF_METAINFO);\n \tconst char *c_reset = diff_get_color_opt(opt, DIFF_RESET);\n+\tconst char *name = strip_relative_prefix(opt, elem->path);\n \tconst char *abb;\n \tint added = 0;\n \tint deleted = 0;\n@@ -942,7 +951,7 @@ static void show_combined_header(struct combine_diff_path *elem,\n \t\tshow_log(rev);\n \n \tdump_quoted_path(dense ? \"diff --cc \" : \"diff --combined \",\n-\t\t\t \"\", elem->path, line_prefix, c_meta, c_reset);\n+\t\t\t \"\", name, line_prefix, c_meta, c_reset);\n \tprintf(\"%s%sindex \", line_prefix, c_meta);\n \tfor (i = 0; i < num_parent; i++) {\n \t\tabb = repo_find_unique_abbrev(the_repository,\n@@ -987,6 +996,7 @@ static void show_combined_header(struct combine_diff_path *elem,\n \t\t\tconst char *path = elem->parent[i].path ?\n \t\t\t\t\t   elem->parent[i].path :\n \t\t\t\t\t   elem->path;\n+\t\t\tpath = strip_relative_prefix(opt, path);\n \t\t\tif (elem->parent[i].status == DIFF_STATUS_ADDED)\n \t\t\t\tdump_quoted_path(\"--- \", \"\", \"/dev/null\",\n \t\t\t\t\t\t line_prefix, c_meta, c_reset);\n@@ -999,14 +1009,14 @@ static void show_combined_header(struct combine_diff_path *elem,\n \t\t\tdump_quoted_path(\"--- \", \"\", \"/dev/null\",\n \t\t\t\t\t line_prefix, c_meta, c_reset);\n \t\telse\n-\t\t\tdump_quoted_path(\"--- \", a_prefix, elem->path,\n+\t\t\tdump_quoted_path(\"--- \", a_prefix, name,\n \t\t\t\t\t line_prefix, c_meta, c_reset);\n \t}\n \tif (deleted)\n \t\tdump_quoted_path(\"+++ \", \"\", \"/dev/null\",\n \t\t\t\t line_prefix, c_meta, c_reset);\n \telse\n-\t\tdump_quoted_path(\"+++ \", b_prefix, elem->path,\n+\t\tdump_quoted_path(\"+++ \", b_prefix, name,\n \t\t\t\t line_prefix, c_meta, c_reset);\n }\n \n@@ -1270,9 +1280,10 @@ static void show_raw_diff(struct combine_diff_path *p, int num_parent, struct re\n \t\t\tconst char *path = p->parent[i].path ?\n \t\t\t\t\t   p->parent[i].path :\n \t\t\t\t\t   p->path;\n+\t\t\tpath = strip_relative_prefix(opt, path);\n \t\t\twrite_name_quoted(path, stdout, inter_name_termination);\n \t\t}\n-\twrite_name_quoted(p->path, stdout, line_termination);\n+\twrite_name_quoted(strip_relative_prefix(opt, p->path), stdout, line_termination);\n }\n \n /*\ndiff --git a/t/t4038-diff-combined.sh b/t/t4038-diff-combined.sh\nindex e11b711388..2575c06360 100755\n--- a/t/t4038-diff-combined.sh\n+++ b/t/t4038-diff-combined.sh\n@@ -492,6 +492,28 @@ test_expect_success '--combined-all-paths and --cc' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success '--combined-all-paths and --raw with relative renamed paths' '\n+\tcat <<-EOF >expect &&\n+\t::100644 100644 100644 $side1cf $side2cf $mergedf RR\tside1c\tside2c\tmerged\n+\tEOF\n+\tgit diff-tree -c -M --raw --combined-all-paths \\\n+\t\t--relative=filename- HEAD >actual.tmp &&\n+\tsed 1d <actual.tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--combined-all-paths and --cc with relative renamed paths' '\n+\tcat <<-\\EOF >expect &&\n+\t--- a/side1c\n+\t--- a/side2c\n+\t+++ b/merged\n+\tEOF\n+\tgit diff-tree --cc -M --combined-all-paths \\\n+\t\t--relative=filename- HEAD >actual.tmp &&\n+\tgrep ^[-+][-+][-+] <actual.tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success FUNNYNAMES 'setup for --combined-all-paths with funny names' '\n \tgit branch side1d &&\n \tgit branch side2d &&\ndiff --git a/t/t4045-diff-relative.sh b/t/t4045-diff-relative.sh\nindex 167be0bdcc..f105ddcfd1 100755\n--- a/t/t4045-diff-relative.sh\n+++ b/t/t4045-diff-relative.sh\n@@ -223,7 +223,7 @@ test_expect_success 'diff --relative --name-only with change in subdir' '\n \ttest_cmp expected out\n '\n \n-test_expect_failure 'diff --relative with change in subdir' '\n+test_expect_success 'diff --relative with change in subdir' '\n \tgit switch br3 &&\n \tbr1_blob=$(git rev-parse --short --verify br1:subdir/file0) &&\n \tbr3_blob=$(git rev-parse --short --verify br3:subdir/file0) &&\n@@ -242,6 +242,44 @@ test_expect_failure 'diff --relative with change in subdir' '\n \t++>>>>>>> br1\n \tEOF\n \tgit -C subdir diff --relative >out &&\n+\ttest_cmp expected out &&\n+\tgit diff --relative=subdir >out &&\n+\ttest_cmp expected out &&\n+\tgit diff --relative=subdir/ >out &&\n+\ttest_cmp expected out &&\n+\tsed \"s,file0,dir/file0,g\" expected >expected-short &&\n+\tgit diff --relative=sub >out &&\n+\ttest_cmp expected-short out &&\n+\tsed \"s,file0,subdir/file0,g\" expected >expected-full &&\n+\tgit diff --relative=subdir --no-relative -- subdir/file0 >out &&\n+\ttest_cmp expected-full out\n+'\n+\n+test_expect_success 'combined raw diff with relative paths' '\n+\tgit switch br3 &&\n+\tbr1_blob=$(git rev-parse --short --verify br1:subdir/file0) &&\n+\tbr3_blob=$(git rev-parse --short --verify br3:subdir/file0) &&\n+\ttest_when_finished \"git merge --abort\" &&\n+\ttest_must_fail git merge br1 &&\n+\tprintf \"::100644 100644 100644 %s %s 0000000 MM\\tfile0\\n\" \\\n+\t\t\"$br3_blob\" \"$br1_blob\" >expected &&\n+\tgit -C subdir diff --cc --raw --relative >out &&\n+\ttest_cmp expected out &&\n+\tprintf \"::100644 100644 100644 %s %s 0000000 MM\\0file0\\0\" \\\n+\t\t\"$br3_blob\" \"$br1_blob\" >expected &&\n+\tgit diff --cc --raw -z --relative=subdir >out &&\n+\ttest_cmp expected out\n+'\n+\n+test_expect_success 'combined raw diff lists all relative paths' '\n+\tgit switch br3 &&\n+\tbr1_blob=$(git rev-parse --short --verify br1:subdir/file0) &&\n+\tbr3_blob=$(git rev-parse --short --verify br3:subdir/file0) &&\n+\ttest_when_finished \"git merge --abort\" &&\n+\ttest_must_fail git merge br1 &&\n+\tprintf \"::100644 100644 100644 %s %s 0000000 MM\\tfile0\\tfile0\\tfile0\\n\" \\\n+\t\t\"$br3_blob\" \"$br1_blob\" >expected &&\n+\tgit diff --cc --raw --combined-all-paths --relative=subdir >out &&\n \ttest_cmp expected out\n '\n \n@@ -254,4 +292,26 @@ test_expect_success 'diff --relative --cached with change in subdir' '\n \ttest_cmp expected out\n '\n \n+test_expect_success FUNNYNAMES 'combined diff quotes relative paths' '\n+\ttest_create_repo quoted &&\n+\t(\n+\t\tcd quoted &&\n+\t\tmkdir subdir &&\n+\t\ttest_commit --no-tag base \"subdir/quoted\\\"file\" base &&\n+\t\tgit switch -c side &&\n+\t\ttest_commit --no-tag side \"subdir/quoted\\\"file\" side &&\n+\t\tgit switch -c other HEAD^ &&\n+\t\ttest_commit --no-tag other \"subdir/quoted\\\"file\" other &&\n+\t\ttest_must_fail git merge side &&\n+\t\tcat >expected <<-\\EOF &&\n+\t\tdiff --cc \"quoted\\\"file\"\n+\t\t--- \"a/quoted\\\"file\"\n+\t\t+++ \"b/quoted\\\"file\"\n+\t\tEOF\n+\t\tgit -C subdir diff --relative >out &&\n+\t\tsed -n \"/^diff --cc /p; /^--- /p; /^+++ /p\" out >actual &&\n+\t\ttest_cmp expected actual\n+\t)\n+'\n+\n test_done\n\nbase-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd\n-- \n2.55.0\n\n\n"},{"id":"554385","messageId":"xmqqld89bmd1.fsf@gitster.g","threadId":"66480","inReplyTo":"20261007051734.62590-1-dilsheddilu123@gmail.com","subject":"Re: [PATCH] combine-diff: honor --relative when printing paths","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-07T14:45:30Z","receivedAt":"2026-10-07T14:45:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Muhammed Dilshad A <dilsheddilu123@gmail.com> writes:\n\n> +static const char *strip_relative_prefix(const struct diff_options *opt,\n> +\t\t\t\t\tconst char *path)\n> +{\n> +\tif (opt->prefix && skip_prefix(path, opt->prefix, &path) && *path == '/')\n> +\t\tpath++;\n> +\treturn path;\n> +}\n\nThat's quite a long line.  Read about coding guidelines in our\nDocumentation/ directory.\n\nAlso, do callers guarantee that path may have only up to one\ntrailing slashes and never two or more?\n\n> @@ -932,6 +940,7 @@ static void show_combined_header(struct combine_diff_path *elem,\n>  \tconst char *b_prefix = opt->b_prefix ? opt->b_prefix : \"b/\";\n>  \tconst char *c_meta = diff_get_color_opt(opt, DIFF_METAINFO);\n>  \tconst char *c_reset = diff_get_color_opt(opt, DIFF_RESET);\n> +\tconst char *name = strip_relative_prefix(opt, elem->path);\n>  \tconst char *abb;\n>  \tint added = 0;\n>  \tint deleted = 0;\n> @@ -942,7 +951,7 @@ static void show_combined_header(struct combine_diff_path *elem,\n>  \t\tshow_log(rev);\n>  \n>  \tdump_quoted_path(dense ? \"diff --cc \" : \"diff --combined \",\n> -\t\t\t \"\", elem->path, line_prefix, c_meta, c_reset);\n> +\t\t\t \"\", name, line_prefix, c_meta, c_reset);\n>  \tprintf(\"%s%sindex \", line_prefix, c_meta);\n>  \tfor (i = 0; i < num_parent; i++) {\n>  \t\tabb = repo_find_unique_abbrev(the_repository,\n> @@ -987,6 +996,7 @@ static void show_combined_header(struct combine_diff_path *elem,\n>  \t\t\tconst char *path = elem->parent[i].path ?\n>  \t\t\t\t\t   elem->parent[i].path :\n>  \t\t\t\t\t   elem->path;\n> +\t\t\tpath = strip_relative_prefix(opt, path);\n\nWhen a rename is involved (e.g., originally the contents was in\nhere/file we have made our changes in place, while the other side\nmoved the file to there/file and made changes there, these were\nmatched up and are shown as a merge into here/file.  Wouldn't a\nelem->parent[].path point at here/file while another points at\nthere/file in such a case?  What should happen when our prefix is in\n\"here/\"?  I know \"here/file\" should become \"file\", but what about\n\"there/file\" that they bring into the picture?\n\nNot striping anything does give consistent result and would not\nmislead the readers as long as they understand --relative is\nignored.  Contrasting to that, \"we strip if the path is inside our\nprefix, but otherwise we give full path\" would give ambiguous\noutput, wouldn't it?\n\n\n"},{"id":"554388","messageId":"cover.1791390459.git.dilsheddilu123@gmail.com","threadId":"66480","inReplyTo":"xmqqld89bmd1.fsf@gitster.g","subject":"[PATCH v2 0/2] combine-diff: honor relative paths consistently","fromName":"Muhammed Dilshad A","fromEmail":"dilsheddilu123@gmail.com","sentAt":"2026-10-07T16:35:12Z","receivedAt":"2026-10-07T16:35:12Z","isPatch":true,"sender":{"key":"dilsheddilu123@gmail.com","avatar":null},"body":"Hi Junio,\n\nThanks for the review. I checked the path handling and reproduced the\ncross-directory rename case you described.\n\nWith plain -M, the outside source is filtered before rename detection.\nWith --follow, though, Git searches for renames with an unfiltered tree\ncomparison, so an outside parent name can reach the combined output.\nThe revised patch prints there/file as ../there/file when the prefix is\nhere/. All displayed names then use the same base. The tests cover both\ndiscovery paths, including raw and NUL-separated output.\n\nThe index rejects repeated separators, but tree entry parsing does not\nenforce the same check. The helper now skips all separators at the prefix\nboundary, so it does not rely on there being only one. Explicit prefix\narguments stay literal, matching ordinary diff's filtering behavior.\nI also wrapped the added C lines to fit the coding guidelines.\n\nWhile checking the two discovery paths, I found that the fast multi-tree\nscan bypasses the relative-prefix filter entirely. Patch 2 fixes that\nseparately and adds tests for outside paths and repeated separators in\nan explicit prefix.\n\nChanges since v1:\n\n* Use relative_path() for parent names outside the prefix while keeping\n  ordinary diff's literal-prefix behavior for matching names.\n* Preserve /dev/null, skip all boundary separators, and wrap long lines.\n* Add cross-directory rename tests and the separate fast-scan fix.\n\nThe developer build with SANITIZE=leak succeeds. The affected suites pass\nall 77 normal tests with SHA-1 and SHA-256. A separate run with\nLSAN_OPTIONS=detect_leaks=1 also passes without a leak report. The existing\nthree-parent coalescing failure in t4038 remains an expected failure.\n\nMuhammed Dilshad A (2):\n  combine-diff: honor --relative when printing paths\n  combine-diff: filter the fast scan by the relative prefix\n\n combine-diff.c           |  70 ++++++++++++++++++---\n t/t4038-diff-combined.sh | 130 +++++++++++++++++++++++++++++++++++++++\n t/t4045-diff-relative.sh |  62 ++++++++++++++++++-\n 3 files changed, 251 insertions(+), 11 deletions(-)\n\n\nbase-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd\n-- \n2.55.0\n\n"},{"id":"554389","messageId":"c0cc486a922f734c0785beca2b74e72bac80c1fd.1791390459.git.dilsheddilu123@gmail.com","threadId":"66480","inReplyTo":"cover.1791390459.git.dilsheddilu123@gmail.com","subject":"[PATCH v2 1/2] combine-diff: honor --relative when printing paths","fromName":"Muhammed Dilshad A","fromEmail":"dilsheddilu123@gmail.com","sentAt":"2026-10-07T16:35:13Z","receivedAt":"2026-10-07T16:35:13Z","isPatch":true,"sender":{"key":"dilsheddilu123@gmail.com","avatar":null},"body":"During a merge conflict, \"git diff --relative\" in a subdirectory still\nprints repository-root paths in combined output. Combined raw output has\nthe same problem.\n\nPrint all names relative to the requested prefix, keeping the stored\npaths for reading file contents. Preserve ordinary diff's literal-prefix\nbehavior for matching names, and use relative_path() for renamed parent\nnames outside the prefix. With --relative=here/, there/file is shown as\n../there/file instead of mixing root-relative and relative names.\n\nThe outside-parent case occurs with --follow, which searches for renames\nusing an unfiltered tree comparison. Add cross-directory tests for that\ncase, including raw and NUL-separated output. Keep /dev/null unchanged\nand skip all separators at the prefix boundary.\n\nTurn the known failure in t4045 into a passing test and retain coverage\nfor explicit prefixes, --no-relative and quoted filenames.\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Muhammed Dilshad A <dilsheddilu123@gmail.com>\n---\n combine-diff.c           |  52 +++++++++++++++----\n t/t4038-diff-combined.sh | 109 +++++++++++++++++++++++++++++++++++++++\n t/t4045-diff-relative.sh |  62 +++++++++++++++++++++-\n 3 files changed, 212 insertions(+), 11 deletions(-)\n\ndiff --git a/combine-diff.c b/combine-diff.c\nindex 717d537688..d615471717 100644\n--- a/combine-diff.c\n+++ b/combine-diff.c\n@@ -2,6 +2,7 @@\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n+#include \"abspath.h\"\n #include \"odb.h\"\n #include \"commit.h\"\n #include \"convert.h\"\n@@ -10,6 +11,7 @@\n #include \"environment.h\"\n #include \"hex.h\"\n #include \"object-name.h\"\n+#include \"path.h\"\n #include \"quote.h\"\n #include \"xdiff-interface.h\"\n #include \"xdiff/xmacros.h\"\n@@ -902,19 +904,39 @@ static void reuse_combine_diff(struct sline *sline, unsigned long cnt,\n \tsline->p_lno[i] = sline->p_lno[j];\n }\n \n-static void dump_quoted_path(const char *head,\n+static const char *relative_combined_path(const struct diff_options *opt,\n+\t\t\t\t\t const char *path, struct strbuf *sb)\n+{\n+\tif (!opt->prefix || is_absolute_path(path))\n+\t\treturn path;\n+\n+\t/* Match ordinary diff's literal-prefix handling inside the prefix. */\n+\tif (skip_prefix(path, opt->prefix, &path)) {\n+\t\twhile (*path == '/')\n+\t\t\tpath++;\n+\t\treturn path;\n+\t}\n+\n+\treturn relative_path(path, opt->prefix, sb);\n+}\n+\n+static void dump_quoted_path(const struct diff_options *opt,\n+\t\t\t     const char *head,\n \t\t\t     const char *prefix,\n \t\t\t     const char *path,\n \t\t\t     const char *line_prefix,\n \t\t\t     const char *c_meta, const char *c_reset)\n {\n \tstatic struct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf relative = STRBUF_INIT;\n \n+\tpath = relative_combined_path(opt, path, &relative);\n \tstrbuf_reset(&buf);\n \tstrbuf_addstr(&buf, line_prefix);\n \tstrbuf_addstr(&buf, c_meta);\n \tstrbuf_addstr(&buf, head);\n \tquote_two_c_style(&buf, prefix, path, 0);\n+\tstrbuf_release(&relative);\n \tstrbuf_addstr(&buf, c_reset);\n \tputs(buf.buf);\n }\n@@ -941,7 +963,7 @@ static void show_combined_header(struct combine_diff_path *elem,\n \tif (rev->loginfo && !rev->no_commit_id)\n \t\tshow_log(rev);\n \n-\tdump_quoted_path(dense ? \"diff --cc \" : \"diff --combined \",\n+\tdump_quoted_path(opt, dense ? \"diff --cc \" : \"diff --combined \",\n \t\t\t \"\", elem->path, line_prefix, c_meta, c_reset);\n \tprintf(\"%s%sindex \", line_prefix, c_meta);\n \tfor (i = 0; i < num_parent; i++) {\n@@ -988,25 +1010,25 @@ static void show_combined_header(struct combine_diff_path *elem,\n \t\t\t\t\t   elem->parent[i].path :\n \t\t\t\t\t   elem->path;\n \t\t\tif (elem->parent[i].status == DIFF_STATUS_ADDED)\n-\t\t\t\tdump_quoted_path(\"--- \", \"\", \"/dev/null\",\n+\t\t\t\tdump_quoted_path(opt, \"--- \", \"\", \"/dev/null\",\n \t\t\t\t\t\t line_prefix, c_meta, c_reset);\n \t\t\telse\n-\t\t\t\tdump_quoted_path(\"--- \", a_prefix, path,\n+\t\t\t\tdump_quoted_path(opt, \"--- \", a_prefix, path,\n \t\t\t\t\t\t line_prefix, c_meta, c_reset);\n \t\t}\n \t} else {\n \t\tif (added)\n-\t\t\tdump_quoted_path(\"--- \", \"\", \"/dev/null\",\n+\t\t\tdump_quoted_path(opt, \"--- \", \"\", \"/dev/null\",\n \t\t\t\t\t line_prefix, c_meta, c_reset);\n \t\telse\n-\t\t\tdump_quoted_path(\"--- \", a_prefix, elem->path,\n+\t\t\tdump_quoted_path(opt, \"--- \", a_prefix, elem->path,\n \t\t\t\t\t line_prefix, c_meta, c_reset);\n \t}\n \tif (deleted)\n-\t\tdump_quoted_path(\"+++ \", \"\", \"/dev/null\",\n+\t\tdump_quoted_path(opt, \"+++ \", \"\", \"/dev/null\",\n \t\t\t\t line_prefix, c_meta, c_reset);\n \telse\n-\t\tdump_quoted_path(\"+++ \", b_prefix, elem->path,\n+\t\tdump_quoted_path(opt, \"+++ \", b_prefix, elem->path,\n \t\t\t\t line_prefix, c_meta, c_reset);\n }\n \n@@ -1225,6 +1247,16 @@ static void show_patch_diff(struct combine_diff_path *elem, int num_parent,\n \tfree(sline);\n }\n \n+static void write_combined_path(const struct diff_options *opt,\n+\t\t\t\tconst char *path, int termination)\n+{\n+\tstruct strbuf relative = STRBUF_INIT;\n+\n+\tpath = relative_combined_path(opt, path, &relative);\n+\twrite_name_quoted(path, stdout, termination);\n+\tstrbuf_release(&relative);\n+}\n+\n static void show_raw_diff(struct combine_diff_path *p, int num_parent, struct rev_info *rev)\n {\n \tstruct diff_options *opt = &rev->diffopt;\n@@ -1270,9 +1302,9 @@ static void show_raw_diff(struct combine_diff_path *p, int num_parent, struct re\n \t\t\tconst char *path = p->parent[i].path ?\n \t\t\t\t\t   p->parent[i].path :\n \t\t\t\t\t   p->path;\n-\t\t\twrite_name_quoted(path, stdout, inter_name_termination);\n+\t\t\twrite_combined_path(opt, path, inter_name_termination);\n \t\t}\n-\twrite_name_quoted(p->path, stdout, line_termination);\n+\twrite_combined_path(opt, p->path, line_termination);\n }\n \n /*\ndiff --git a/t/t4038-diff-combined.sh b/t/t4038-diff-combined.sh\nindex e11b711388..21eeb4fbcb 100755\n--- a/t/t4038-diff-combined.sh\n+++ b/t/t4038-diff-combined.sh\n@@ -492,6 +492,28 @@ test_expect_success '--combined-all-paths and --cc' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success '--combined-all-paths and --raw with relative renamed paths' '\n+\tcat <<-EOF >expect &&\n+\t::100644 100644 100644 $side1cf $side2cf $mergedf RR\tside1c\tside2c\tmerged\n+\tEOF\n+\tgit diff-tree -c -M --raw --combined-all-paths \\\n+\t\t--relative=filename- HEAD >actual.tmp &&\n+\tsed 1d <actual.tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--combined-all-paths and --cc with relative renamed paths' '\n+\tcat <<-\\EOF >expect &&\n+\t--- a/side1c\n+\t--- a/side2c\n+\t+++ b/merged\n+\tEOF\n+\tgit diff-tree --cc -M --combined-all-paths \\\n+\t\t--relative=filename- HEAD >actual.tmp &&\n+\tgrep ^[-+][-+][-+] <actual.tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success FUNNYNAMES 'setup for --combined-all-paths with funny names' '\n \tgit branch side1d &&\n \tgit branch side2d &&\n@@ -543,4 +565,91 @@ test_expect_success FUNNYNAMES '--combined-all-paths and --cc and funny names' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'setup rename across relative prefix' '\n+\ttest_create_repo cross-prefix &&\n+\t(\n+\t\tcd cross-prefix &&\n+\t\tmkdir here there &&\n+\t\ttest_seq 1 8 >here/file &&\n+\t\techo base >there/extra &&\n+\t\tgit add here/file there/extra &&\n+\t\tgit commit -m base &&\n+\t\tgit tag base &&\n+\t\tgit switch -c ours &&\n+\t\ttest_seq 1 10 >here/file &&\n+\t\techo ours >there/extra &&\n+\t\tgit add here/file there/extra &&\n+\t\tgit commit -m ours &&\n+\t\tgit switch -c theirs base &&\n+\t\tgit mv here/file there/file &&\n+\t\ttest_seq 1 9 >there/file &&\n+\t\techo ten >>there/file &&\n+\t\techo theirs >there/extra &&\n+\t\tgit add there/file there/extra &&\n+\t\tgit commit -m theirs &&\n+\t\tgit switch ours &&\n+\t\ttest_must_fail git merge --no-commit theirs &&\n+\t\tgit rm -f there/file &&\n+\t\tmkdir -p here &&\n+\t\ttest_seq 1 9 >here/file &&\n+\t\techo ten >>here/file &&\n+\t\techo eleven >>here/file &&\n+\t\techo merged >there/extra &&\n+\t\tgit add here/file there/extra &&\n+\t\tgit commit -m merged\n+\t)\n+'\n+\n+test_expect_success 'combined relative diff excludes an outside rename source' '\n+\tcat >expect <<-\\EOF &&\n+\tdiff --cc file\n+\t--- a/file\n+\t--- /dev/null\n+\t+++ b/file\n+\tEOF\n+\tgit -C cross-prefix diff-tree --no-commit-id --cc -M \\\n+\t\t--combined-all-paths --relative=here/ HEAD >actual.tmp &&\n+\tsed -n \"/^diff --cc /p; /^--- /p; /^+++ /p\" actual.tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'combined raw relative diff excludes an outside rename source' '\n+\tours_oid=$(git -C cross-prefix rev-parse HEAD^:here/file) &&\n+\tmerged_oid=$(git -C cross-prefix rev-parse HEAD:here/file) &&\n+\tprintf \"::100644 000000 100644 %s %s %s MA\\tfile\\tfile\\tfile\\n\" \\\n+\t\t\"$ours_oid\" \"$ZERO_OID\" \"$merged_oid\" >expect &&\n+\tgit -C cross-prefix diff-tree --no-commit-id -c -M --raw \\\n+\t\t--combined-all-paths --relative=here/ HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'combined relative diff follows an outside rename source' '\n+\tcat >expect <<-\\EOF &&\n+\tdiff --cc file\n+\t--- a/file\n+\t--- a/../there/file\n+\t+++ b/file\n+\tEOF\n+\tgit -C cross-prefix diff-tree --no-commit-id --cc -M --follow \\\n+\t\t--combined-all-paths --relative=here/ HEAD -- here/file >actual.tmp &&\n+\tsed -n \"/^diff --cc /p; /^--- /p; /^+++ /p\" actual.tmp >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'combined raw relative diff follows an outside rename source' '\n+\tours_oid=$(git -C cross-prefix rev-parse HEAD^:here/file) &&\n+\ttheirs_oid=$(git -C cross-prefix rev-parse HEAD^2:there/file) &&\n+\tmerged_oid=$(git -C cross-prefix rev-parse HEAD:here/file) &&\n+\tprintf \"::100644 100644 100644 %s %s %s MR\\tfile\\t../there/file\\tfile\\n\" \\\n+\t\t\"$ours_oid\" \"$theirs_oid\" \"$merged_oid\" >expect &&\n+\tgit -C cross-prefix diff-tree --no-commit-id -c -M --follow --raw \\\n+\t\t--combined-all-paths --relative=here/ HEAD -- here/file >actual &&\n+\ttest_cmp expect actual &&\n+\tprintf \"::100644 100644 100644 %s %s %s MR\\0file\\0../there/file\\0file\\0\" \\\n+\t\t\"$ours_oid\" \"$theirs_oid\" \"$merged_oid\" >expect &&\n+\tgit -C cross-prefix diff-tree --no-commit-id -c -M --follow --raw -z \\\n+\t\t--combined-all-paths --relative=here/ HEAD -- here/file >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\ndiff --git a/t/t4045-diff-relative.sh b/t/t4045-diff-relative.sh\nindex 167be0bdcc..f105ddcfd1 100755\n--- a/t/t4045-diff-relative.sh\n+++ b/t/t4045-diff-relative.sh\n@@ -223,7 +223,7 @@ test_expect_success 'diff --relative --name-only with change in subdir' '\n \ttest_cmp expected out\n '\n \n-test_expect_failure 'diff --relative with change in subdir' '\n+test_expect_success 'diff --relative with change in subdir' '\n \tgit switch br3 &&\n \tbr1_blob=$(git rev-parse --short --verify br1:subdir/file0) &&\n \tbr3_blob=$(git rev-parse --short --verify br3:subdir/file0) &&\n@@ -242,6 +242,44 @@ test_expect_failure 'diff --relative with change in subdir' '\n \t++>>>>>>> br1\n \tEOF\n \tgit -C subdir diff --relative >out &&\n+\ttest_cmp expected out &&\n+\tgit diff --relative=subdir >out &&\n+\ttest_cmp expected out &&\n+\tgit diff --relative=subdir/ >out &&\n+\ttest_cmp expected out &&\n+\tsed \"s,file0,dir/file0,g\" expected >expected-short &&\n+\tgit diff --relative=sub >out &&\n+\ttest_cmp expected-short out &&\n+\tsed \"s,file0,subdir/file0,g\" expected >expected-full &&\n+\tgit diff --relative=subdir --no-relative -- subdir/file0 >out &&\n+\ttest_cmp expected-full out\n+'\n+\n+test_expect_success 'combined raw diff with relative paths' '\n+\tgit switch br3 &&\n+\tbr1_blob=$(git rev-parse --short --verify br1:subdir/file0) &&\n+\tbr3_blob=$(git rev-parse --short --verify br3:subdir/file0) &&\n+\ttest_when_finished \"git merge --abort\" &&\n+\ttest_must_fail git merge br1 &&\n+\tprintf \"::100644 100644 100644 %s %s 0000000 MM\\tfile0\\n\" \\\n+\t\t\"$br3_blob\" \"$br1_blob\" >expected &&\n+\tgit -C subdir diff --cc --raw --relative >out &&\n+\ttest_cmp expected out &&\n+\tprintf \"::100644 100644 100644 %s %s 0000000 MM\\0file0\\0\" \\\n+\t\t\"$br3_blob\" \"$br1_blob\" >expected &&\n+\tgit diff --cc --raw -z --relative=subdir >out &&\n+\ttest_cmp expected out\n+'\n+\n+test_expect_success 'combined raw diff lists all relative paths' '\n+\tgit switch br3 &&\n+\tbr1_blob=$(git rev-parse --short --verify br1:subdir/file0) &&\n+\tbr3_blob=$(git rev-parse --short --verify br3:subdir/file0) &&\n+\ttest_when_finished \"git merge --abort\" &&\n+\ttest_must_fail git merge br1 &&\n+\tprintf \"::100644 100644 100644 %s %s 0000000 MM\\tfile0\\tfile0\\tfile0\\n\" \\\n+\t\t\"$br3_blob\" \"$br1_blob\" >expected &&\n+\tgit diff --cc --raw --combined-all-paths --relative=subdir >out &&\n \ttest_cmp expected out\n '\n \n@@ -254,4 +292,26 @@ test_expect_success 'diff --relative --cached with change in subdir' '\n \ttest_cmp expected out\n '\n \n+test_expect_success FUNNYNAMES 'combined diff quotes relative paths' '\n+\ttest_create_repo quoted &&\n+\t(\n+\t\tcd quoted &&\n+\t\tmkdir subdir &&\n+\t\ttest_commit --no-tag base \"subdir/quoted\\\"file\" base &&\n+\t\tgit switch -c side &&\n+\t\ttest_commit --no-tag side \"subdir/quoted\\\"file\" side &&\n+\t\tgit switch -c other HEAD^ &&\n+\t\ttest_commit --no-tag other \"subdir/quoted\\\"file\" other &&\n+\t\ttest_must_fail git merge side &&\n+\t\tcat >expected <<-\\EOF &&\n+\t\tdiff --cc \"quoted\\\"file\"\n+\t\t--- \"a/quoted\\\"file\"\n+\t\t+++ \"b/quoted\\\"file\"\n+\t\tEOF\n+\t\tgit -C subdir diff --relative >out &&\n+\t\tsed -n \"/^diff --cc /p; /^--- /p; /^+++ /p\" out >actual &&\n+\t\ttest_cmp expected actual\n+\t)\n+'\n+\n test_done\n-- \n2.55.0\n\n\n"},{"id":"554390","messageId":"dbc32586d8ee3b41bb28cd9f6e838658b2206880.1791390459.git.dilsheddilu123@gmail.com","threadId":"66480","inReplyTo":"cover.1791390459.git.dilsheddilu123@gmail.com","subject":"[PATCH v2 2/2] combine-diff: filter the fast scan by the relative prefix","fromName":"Muhammed Dilshad A","fromEmail":"dilsheddilu123@gmail.com","sentAt":"2026-10-07T16:35:14Z","receivedAt":"2026-10-07T16:35:14Z","isPatch":true,"sender":{"key":"dilsheddilu123@gmail.com","avatar":null},"body":"The pairwise scan filters paths by --relative before looking for renames.\nThe fast scan bypasses those callbacks, so it can show changes outside\nthe requested prefix.\n\nFilter the fast scan's paths by the same literal prefix and release the\ndiscarded records. Add tests for excluding outside paths and for keeping\nexplicit prefixes with repeated separators literal, as ordinary diff\ndoes.\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Muhammed Dilshad A <dilsheddilu123@gmail.com>\n---\n combine-diff.c           | 18 ++++++++++++++++++\n t/t4038-diff-combined.sh | 21 +++++++++++++++++++++\n 2 files changed, 39 insertions(+)\n\ndiff --git a/combine-diff.c b/combine-diff.c\nindex d615471717..e8a3bd1ee9 100644\n--- a/combine-diff.c\n+++ b/combine-diff.c\n@@ -1474,6 +1474,24 @@ static struct combine_diff_path *find_paths_multitree(\n \n \tstrbuf_release(&base);\n \tfree(parents_oid);\n+\n+\t/* Match the prefix filtering used by the pairwise scan. */\n+\tif (opt->prefix) {\n+\t\tstruct combine_diff_path **tail = &paths;\n+\n+\t\twhile (*tail) {\n+\t\t\tstruct combine_diff_path *p = *tail;\n+\n+\t\t\tif (starts_with(p->path, opt->prefix)) {\n+\t\t\t\ttail = &p->next;\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\t*tail = p->next;\n+\t\t\tfor (i = 0; i < nparent; i++)\n+\t\t\t\tfree(p->parent[i].path);\n+\t\t\tfree(p);\n+\t\t}\n+\t}\n \treturn paths;\n }\n \ndiff --git a/t/t4038-diff-combined.sh b/t/t4038-diff-combined.sh\nindex 21eeb4fbcb..50df3dfb46 100755\n--- a/t/t4038-diff-combined.sh\n+++ b/t/t4038-diff-combined.sh\n@@ -652,4 +652,25 @@ test_expect_success 'combined raw relative diff follows an outside rename source\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fast combined relative diff excludes outside paths' '\n+\tours_oid=$(git -C cross-prefix rev-parse HEAD^:here/file) &&\n+\tmerged_oid=$(git -C cross-prefix rev-parse HEAD:here/file) &&\n+\tprintf \"::100644 000000 100644 %s %s %s MA\\tfile\\tfile\\tfile\\n\" \\\n+\t\t\"$ours_oid\" \"$ZERO_OID\" \"$merged_oid\" >expect &&\n+\tgit -C cross-prefix diff-tree --no-commit-id -c --raw --no-renames \\\n+\t\t--combined-all-paths --relative=here/ HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'combined diff keeps explicit relative prefixes literal' '\n+\tgit -C cross-prefix diff HEAD^ HEAD --relative=here// >actual &&\n+\ttest_must_be_empty actual &&\n+\tgit -C cross-prefix diff-tree --no-commit-id --cc -M \\\n+\t\t--combined-all-paths --relative=here// HEAD >actual &&\n+\ttest_must_be_empty actual &&\n+\tgit -C cross-prefix diff-tree --no-commit-id --cc --no-renames \\\n+\t\t--combined-all-paths --relative=here// HEAD >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n test_done\n-- \n2.55.0\n\n\n"},{"id":"554399","messageId":"xmqqh5ix8kji.fsf@gitster.g","threadId":"66480","inReplyTo":"cover.1791390459.git.dilsheddilu123@gmail.com","subject":"Re: [PATCH v2 0/2] combine-diff: honor relative paths consistently","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-07T17:53:05Z","receivedAt":"2026-10-07T17:53:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Muhammed Dilshad A <dilsheddilu123@gmail.com> writes:\n\n> The revised patch prints there/file as ../there/file when the prefix is\n> here/. All displayed names then use the same base. The tests cover both\n> discovery paths, including raw and NUL-separated output.\n\nThis sounds like the most sensible behaviour, within the constraint\nthat --relative must give a relative path to the prefix.\n\n> The index rejects repeated separators, but tree entry parsing does not\n> enforce the same check. The helper now skips all separators at the prefix\n> boundary, so it does not rely on there being only one. Explicit prefix\n> arguments stay literal, matching ordinary diff's filtering behavior.\n> I also wrapped the added C lines to fit the coding guidelines.\n\nDo *not* respond to review comments in your cover letter.  Nobody\nreading the above, other than those who have seen our earlier\nexchange of you sending v1 patch with I commenting on it, would not\nknow what you are talking about in the above, and especially what is\nso special about \"repeated separators\" without context.  The cover\nletter should aim to welcome even those late-comming reviewers who\nmissed an earlier round.\n\nReview response should be done as a response to a review message,\nunrelated to your rerolled patches.\n\n> While checking the two discovery paths, I found that the fast multi-tree\n> scan bypasses the relative-prefix filter entirely. Patch 2 fixes that\n> separately and adds tests for outside paths and repeated separators in\n> an explicit prefix.\n\nGreat.\n\n>\n> Changes since v1:\n>\n> * Use relative_path() for parent names outside the prefix while keeping\n>   ordinary diff's literal-prefix behavior for matching names.\n> * Preserve /dev/null, skip all boundary separators, and wrap long lines.\n> * Add cross-directory rename tests and the separate fast-scan fix.\n>\n> The developer build with SANITIZE=leak succeeds. The affected suites pass\n> all 77 normal tests with SHA-1 and SHA-256. A separate run with\n> LSAN_OPTIONS=detect_leaks=1 also passes without a leak report. The existing\n> three-parent coalescing failure in t4038 remains an expected failure.\n>\n> Muhammed Dilshad A (2):\n>   combine-diff: honor --relative when printing paths\n>   combine-diff: filter the fast scan by the relative prefix\n>\n>  combine-diff.c           |  70 ++++++++++++++++++---\n>  t/t4038-diff-combined.sh | 130 +++++++++++++++++++++++++++++++++++++++\n>  t/t4045-diff-relative.sh |  62 ++++++++++++++++++-\n>  3 files changed, 251 insertions(+), 11 deletions(-)\n>\n>\n> base-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd\n\n"}]}