Skip to content

Commit bfab1d2

Browse files
committed
Better handling for git diffs in filterdiff/lsdiff
Assisted-by: Cursor
1 parent b073854 commit bfab1d2

10 files changed

Lines changed: 743 additions & 116 deletions

File tree

‎Makefile.am‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,11 @@ TESTS = tests/newline1/run-test \
281281
tests/patch-ignore-whitespace/run-test \
282282
tests/whitespace-w/run-test \
283283
tests/filterdiff-inplace1/run-test \
284-
tests/rediff-inplace1/run-test
284+
tests/rediff-inplace1/run-test \
285+
tests/git-rename-issue22/run-test \
286+
tests/git-binary-issue57/run-test \
287+
tests/git-mode-issue59/run-test \
288+
tests/git-exclude-issue27/run-test
285289

286290
# These ones don't work yet.
287291
# Feel free to send me patches. :-)

‎NEWS‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ Patchutils news
33

44
0.4.4 (stable)
55

6+
Enhanced Git diff format support in filterdiff/lsdiff/grepdiff.
7+
Improved handling of Git diffs without traditional hunks,
8+
including proper support for file renames, binary file changes,
9+
and permission mode changes. Fixed exclusion filtering (-x) for
10+
multi-file Git diffs to prevent orphaned headers. Enhanced filename
11+
extraction to properly strip Git's a/ and b/ prefixes for consistent
12+
output. Addresses GitHub issues #22, #27, #57, and #59.
13+
614
Code improvements and build system enhancements. Added redirectfd()
715
utility function to redirect stdout without reassignment. Enhanced
816
CI testing with musl support for better compatibility testing.

‎doc/patchutils.xml‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,10 @@
724724

725725
<screen>filterdiff message-with-diff-in-the-body > patch</screen></para>
726726

727+
<para>Filterdiff supports unified, context, and Git format diffs.
728+
Git format includes support for binary files, file renames,
729+
permission mode changes, and other Git-specific diff features.</para>
730+
727731
<para>Note that the interpretation of the shell wildcard
728732
pattern does not count slash characters or periods as
729733
special (in other words, no flags are given to
@@ -1039,6 +1043,12 @@ patch.file]]></screen></para>
10391043
and context format diffs:
10401044

10411045
<screen><![CDATA[filterdiff -v --format=unified context.diff]]></screen></para>
1046+
1047+
<para>Git format diffs are fully supported, including binary files,
1048+
file renames, and permission mode changes. For example, to extract
1049+
only the renames from a git diff:
1050+
1051+
<screen><![CDATA[filterdiff -i '*/newname' git-patch-with-renames.patch]]></screen></para>
10421052
</refsect1>
10431053

10441054
<refsect1>
@@ -1243,8 +1253,10 @@ patch.file]]></screen></para>
12431253

12441254
<para>List the files modified by a patch.</para>
12451255

1246-
<para>You can use both unified and context format diffs with
1247-
this program.</para>
1256+
<para>You can use unified, context, and Git format diffs with
1257+
this program. Git format includes support for binary files,
1258+
file renames, permission mode changes, and other Git-specific
1259+
diff features.</para>
12481260

12491261
</refsect1>
12501262

@@ -2291,8 +2303,10 @@ will pipe patch of file #2 to vim - -R
22912303
<screen><![CDATA[grepdiff pf_gfp_mask my.patch | \
22922304
xargs -rn1 filterdiff my.patch -i]]></screen></para>
22932305

2294-
<para>You can use both unified and context format diffs with
2295-
this program.</para>
2306+
<para>You can use unified, context, and Git format diffs with
2307+
this program. Git format includes support for binary files,
2308+
file renames, permission mode changes, and other Git-specific
2309+
diff features.</para>
22962310

22972311
</refsect1>
22982312

@@ -2603,6 +2617,20 @@ will pipe patch of file #2 to vim - -R
26032617
</variablelist>
26042618
</refsect1>
26052619

2620+
<refsect1>
2621+
<title>Examples</title>
2622+
2623+
<para>Git format diffs are fully supported. For example, to find
2624+
files in a git patch that contain changes to malloc calls:
2625+
2626+
<screen><![CDATA[grepdiff "malloc" git-patch.patch]]></screen></para>
2627+
2628+
<para>Note that grepdiff searches in the hunk content (the actual
2629+
code changes), not in Git metadata like rename headers. Files with
2630+
only renames or mode changes (no content hunks) won't be found
2631+
even if the pattern appears in the Git headers.</para>
2632+
</refsect1>
2633+
26062634
<refsect1>
26072635
<title>See also</title>
26082636

‎src/diff.c‎

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,7 @@ static FILE *do_convert (FILE *f, const char *mode, int seekable,
939939
error (EXIT_FAILURE, errno, "fdopen failed");
940940

941941
if (seekable) {
942-
FILE *tmp = xtmpfile ();
942+
FILE *tmp = xtmpfile ();
943943
while (!feof (ret)) {
944944
int c = fgetc (ret);
945945

@@ -1112,3 +1112,118 @@ filename_from_header (const char *header)
11121112

11131113
return xstrndup (header, h);
11141114
}
1115+
1116+
enum git_diff_type
1117+
detect_git_diff_type (char **headers, unsigned int num_headers)
1118+
{
1119+
unsigned int i;
1120+
int has_similarity_100 = 0;
1121+
int has_rename = 0;
1122+
int has_binary = 0;
1123+
int has_mode_change = 0;
1124+
int has_new_file = 0;
1125+
int has_deleted_file = 0;
1126+
1127+
/* Check if this is even a git diff */
1128+
if (num_headers == 0 || strncmp (headers[0], "diff --git ", 11))
1129+
return GIT_DIFF_NORMAL;
1130+
1131+
/* Analyze the headers */
1132+
for (i = 0; i < num_headers; i++) {
1133+
if (!strncmp (headers[i], "similarity index 100%", 21))
1134+
has_similarity_100 = 1;
1135+
else if (!strncmp (headers[i], "rename from ", 12) ||
1136+
!strncmp (headers[i], "rename to ", 10))
1137+
has_rename = 1;
1138+
else if (!strncmp (headers[i], "old mode ", 9) ||
1139+
!strncmp (headers[i], "new mode ", 9))
1140+
has_mode_change = 1;
1141+
else if (!strncmp (headers[i], "new file mode ", 14))
1142+
has_new_file = 1;
1143+
else if (!strncmp (headers[i], "deleted file mode ", 18))
1144+
has_deleted_file = 1;
1145+
else if (strstr (headers[i], "Binary files ") ||
1146+
!strncmp (headers[i], "GIT binary patch", 16))
1147+
has_binary = 1;
1148+
}
1149+
1150+
/* Determine the type based on header combinations */
1151+
if (has_similarity_100 && has_rename)
1152+
return GIT_DIFF_RENAME;
1153+
else if (has_binary)
1154+
return GIT_DIFF_BINARY;
1155+
else if (has_mode_change && !has_new_file && !has_deleted_file)
1156+
return GIT_DIFF_MODE_ONLY;
1157+
else if (has_new_file)
1158+
return GIT_DIFF_NEW_FILE;
1159+
else if (has_deleted_file)
1160+
return GIT_DIFF_DELETED_FILE;
1161+
else
1162+
return GIT_DIFF_NORMAL;
1163+
}
1164+
1165+
int
1166+
extract_git_filenames (char **headers, unsigned int num_headers,
1167+
char **old_name, char **new_name)
1168+
{
1169+
unsigned int i;
1170+
char *git_line = NULL;
1171+
char *rename_from = NULL, *rename_to = NULL;
1172+
1173+
*old_name = NULL;
1174+
*new_name = NULL;
1175+
1176+
/* Find the diff --git line */
1177+
for (i = 0; i < num_headers; i++) {
1178+
if (!strncmp (headers[i], "diff --git ", 11)) {
1179+
git_line = headers[i];
1180+
break;
1181+
}
1182+
}
1183+
1184+
if (!git_line)
1185+
return 1; /* Not a git diff */
1186+
1187+
/* Look for rename headers first */
1188+
for (i = 0; i < num_headers; i++) {
1189+
if (!strncmp (headers[i], "rename from ", 12))
1190+
rename_from = headers[i] + 12;
1191+
else if (!strncmp (headers[i], "rename to ", 10))
1192+
rename_to = headers[i] + 10;
1193+
}
1194+
1195+
if (rename_from && rename_to) {
1196+
/* Use rename headers for filenames */
1197+
*old_name = xstrndup (rename_from, strcspn (rename_from, "\n\r"));
1198+
*new_name = xstrndup (rename_to, strcspn (rename_to, "\n\r"));
1199+
} else {
1200+
/* Parse filenames from "diff --git a/path b/path" line */
1201+
char *p = git_line + 11; /* Skip "diff --git " */
1202+
char *space1, *space2;
1203+
1204+
/* Find the space between a/path and b/path */
1205+
space1 = strchr (p, ' ');
1206+
if (!space1)
1207+
return 1;
1208+
1209+
/* Extract old filename, stripping a/ prefix if present */
1210+
if (space1 - p > 2 && p[0] == 'a' && p[1] == '/') {
1211+
*old_name = xstrndup (p + 2, space1 - p - 2);
1212+
} else {
1213+
*old_name = xstrndup (p, space1 - p);
1214+
}
1215+
1216+
/* Find start of new filename (b/path) */
1217+
space2 = space1 + 1;
1218+
1219+
/* Extract new filename, stripping b/ prefix if present */
1220+
size_t new_len = strcspn (space2, "\n\r");
1221+
if (new_len > 2 && space2[0] == 'b' && space2[1] == '/') {
1222+
*new_name = xstrndup (space2 + 2, new_len - 2);
1223+
} else {
1224+
*new_name = xstrndup (space2, new_len);
1225+
}
1226+
}
1227+
1228+
return 0;
1229+
}

‎src/diff.h‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,17 @@ int read_timestamp (const char *timestamp,
5555
long *zone /* may be NULL */);
5656

5757
char *filename_from_header (const char *header);
58+
59+
/* Git diff support */
60+
enum git_diff_type {
61+
GIT_DIFF_NORMAL = 0, /* Regular diff with hunks */
62+
GIT_DIFF_RENAME, /* Pure rename (similarity index 100%) */
63+
GIT_DIFF_BINARY, /* Binary file diff */
64+
GIT_DIFF_MODE_ONLY, /* Mode change only */
65+
GIT_DIFF_NEW_FILE, /* New file creation */
66+
GIT_DIFF_DELETED_FILE /* File deletion */
67+
};
68+
69+
enum git_diff_type detect_git_diff_type (char **headers, unsigned int num_headers);
70+
int extract_git_filenames (char **headers, unsigned int num_headers,
71+
char **old_name, char **new_name);

0 commit comments

Comments
 (0)