diff --git a/Makefile.am b/Makefile.am
index 4c550675..8b43e02d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -281,7 +281,24 @@ TESTS = tests/newline1/run-test \
tests/patch-ignore-whitespace/run-test \
tests/whitespace-w/run-test \
tests/filterdiff-inplace1/run-test \
- tests/rediff-inplace1/run-test
+ tests/rediff-inplace1/run-test \
+ tests/git-rename-issue22/run-test \
+ tests/git-binary-issue57/run-test \
+ tests/git-binary-formats/run-test \
+ tests/git-mode-issue59/run-test \
+ tests/git-exclude-issue27/run-test \
+ tests/git-prefixes-option/run-test \
+ tests/git-prefixes-with-strip/run-test \
+ tests/git-diff-duplication/run-test \
+ tests/git-complex-mixed/run-test \
+ tests/git-error-handling/run-test \
+ tests/git-copy-operations/run-test \
+ tests/git-extended-headers/run-test \
+ tests/git-lsdiff-status/run-test \
+ tests/git-deleted-file/run-test \
+ tests/git-pure-rename/run-test \
+ tests/git-diff-edge-cases/run-test \
+ tests/malformed-diff-headers/run-test
# These ones don't work yet.
# Feel free to send me patches. :-)
diff --git a/NEWS b/NEWS
index e39344d4..3678644e 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,17 @@ Patchutils news
0.4.4 (stable)
+ Enhanced Git diff format support in filterdiff/lsdiff/grepdiff.
+ Improved handling of Git diffs without traditional hunks,
+ including proper support for file renames, binary file changes,
+ and permission mode changes. Fixed exclusion filtering (-x) for
+ multi-file Git diffs to prevent orphaned headers. Enhanced filename
+ extraction to properly strip Git's a/ and b/ prefixes for consistent
+ output. Added --git-prefixes=strip|keep option to control handling
+ of Git filename prefixes, with 'keep' as default for backward
+ compatibility (will change to 'strip' in version 0.5.0).
+ Addresses GitHub issues #22, #27, #59, and #68.
+
Code improvements and build system enhancements. Added redirectfd()
utility function to redirect stdout without reassignment. Enhanced
CI testing with musl support for better compatibility testing.
diff --git a/bash-completion-patchutils b/bash-completion-patchutils
index 388c6fbc..c6f3cf84 100644
--- a/bash-completion-patchutils
+++ b/bash-completion-patchutils
@@ -7,7 +7,7 @@ _patchutils_common_opts() {
}
_patchutils_filter_opts() {
- echo "--exclude -x --exclude-from-file -X --include -i --include-from-file -I --hunks -# --lines --files -F --annotate --as-numbered-lines --format --remove-timestamps --clean --in-place --decompress -z --strip-match -p --strip --addprefix --addoldprefix --addnewprefix"
+ echo "--exclude -x --exclude-from-file -X --include -i --include-from-file -I --hunks -# --lines --files -F --annotate --as-numbered-lines --format --remove-timestamps --clean --in-place --decompress -z --strip-match -p --strip --addprefix --addoldprefix --addnewprefix --git-prefixes"
}
_patchutils_list_opts() {
@@ -74,6 +74,10 @@ _filterdiff() {
# Prefix completion - no specific completion
return 0
;;
+ --git-prefixes)
+ COMPREPLY=($(compgen -W "strip keep" -- "$cur"))
+ return 0
+ ;;
esac
if [[ ${cur} == -* ]]; then
@@ -110,6 +114,10 @@ _lsdiff() {
--addprefix|--addoldprefix|--addnewprefix)
return 0
;;
+ --git-prefixes)
+ COMPREPLY=($(compgen -W "strip keep" -- "$cur"))
+ return 0
+ ;;
esac
if [[ ${cur} == -* ]]; then
@@ -162,6 +170,10 @@ _grepdiff() {
--addprefix|--addoldprefix|--addnewprefix)
return 0
;;
+ --git-prefixes)
+ COMPREPLY=($(compgen -W "strip keep" -- "$cur"))
+ return 0
+ ;;
esac
if [[ ${cur} == -* ]]; then
@@ -374,6 +386,10 @@ _patchview() {
--addprefix|--addoldprefix|--addnewprefix)
return 0
;;
+ --git-prefixes)
+ COMPREPLY=($(compgen -W "strip keep" -- "$cur"))
+ return 0
+ ;;
esac
if [[ ${cur} == -* ]]; then
diff --git a/doc/patchutils.xml b/doc/patchutils.xml
index 7ed44167..656f2554 100644
--- a/doc/patchutils.xml
+++ b/doc/patchutils.xml
@@ -654,6 +654,7 @@
--strip-match=n
--strip=n
+ --git-prefixes=strip|keep
--addprefix=PREFIX
--addoldprefix=PREFIX
--addnewprefix=PREFIX
@@ -724,6 +725,10 @@
filterdiff message-with-diff-in-the-body > patch
+ Filterdiff supports unified, context, and Git format diffs.
+ Git format includes support for binary files, file renames,
+ permission mode changes, and other Git-specific diff features.
+
Note that the interpretation of the shell wildcard
pattern does not count slash characters or periods as
special (in other words, no flags are given to
@@ -860,6 +865,23 @@
+
+ =strip|keep
+
+ How to handle a/ and b/
+ prefixes in Git diff filenames. With strip, removes
+ the prefixes both for filename matching (when using
+ and options) and for filename output (similar to
+ ). With keep (default),
+ preserves existing behavior. Applies to both Git-specific diffs (binary files,
+ renames, mode changes) and traditional diffs when part of a Git patch.
+ Note: Git rename operations use rename from/to headers
+ that contain literal filenames without a/ or
+ b/ prefixes, so this option does not affect renamed
+ filenames. The default will change to strip in version 0.5.0.
+
+
+
=PREFIX
@@ -1039,6 +1061,12 @@ patch.file]]>
and context format diffs:
+
+ Git format diffs are fully supported, including binary files,
+ file renames, and permission mode changes. For example, to extract
+ only the renames from a git diff:
+
+
@@ -1182,6 +1210,7 @@ patch.file]]>
--strip-match=n
--strip=n
+ --git-prefixes=strip|keep
--addprefix=PREFIX
-s
@@ -1243,8 +1272,10 @@ patch.file]]>
List the files modified by a patch.
- You can use both unified and context format diffs with
- this program.
+ You can use unified, context, and Git format diffs with
+ this program. Git format includes support for binary files,
+ file renames, permission mode changes, and other Git-specific
+ diff features.
@@ -1354,6 +1385,23 @@ patch.file]]>
+
+ =strip|keep
+
+ How to handle a/ and b/
+ prefixes in Git diff filenames. With strip, removes
+ the prefixes both for filename matching (when using
+ and options) and for filename output (similar to
+ ). With keep (default),
+ preserves existing behavior. Applies to both Git-specific diffs (binary files,
+ renames, mode changes) and traditional diffs when part of a Git patch.
+ Note: Git rename operations use rename from/to headers
+ that contain literal filenames without a/ or
+ b/ prefixes, so this option does not affect renamed
+ filenames. The default will change to strip in version 0.5.0.
+
+
+
=PREFIX
@@ -1534,6 +1582,7 @@ done)]]>
--strip-match=n
--strip=n
+ --git-prefixes=strip|keep
--addprefix=PREFIX
-s
@@ -1597,13 +1646,13 @@ done)]]>
patchview
(without args)
-
+
is equivalent to: lsdiff --number
patchview -F2-
(or with any other args)
-
+
is equivalent to: filterdiff -F2- (or whatever arguments are supplied)
@@ -1612,19 +1661,19 @@ There are two scripts for working with git (gitdiff and gitdiffview) and two for
svndiff
gitdiff
(without args)
-
+
will give the list of files modified
svndiff -F1
gitdiff -F1
-
+
will show the patch of file #1
svndiffview
gitdiffview
-
+
pipe all patches through filterdiff to vim - -R (in read-only mode, easy to quit), showing complete patch with color.
@@ -1743,6 +1792,23 @@ will pipe patch of file #2 to vim - -R
+
+ =strip|keep
+
+ How to handle a/ and b/
+ prefixes in Git diff filenames. With strip, removes
+ the prefixes both for filename matching (when using
+ and options) and for filename output (similar to
+ ). With keep (default),
+ preserves existing behavior. Applies to both Git-specific diffs (binary files,
+ renames, mode changes) and traditional diffs when part of a Git patch.
+ Note: Git rename operations use rename from/to headers
+ that contain literal filenames without a/ or
+ b/ prefixes, so this option does not affect renamed
+ filenames. The default will change to strip in version 0.5.0.
+
+
+
=PREFIX
@@ -2193,6 +2259,7 @@ will pipe patch of file #2 to vim - -R
--strip-match=n
--strip=n
+ --git-prefixes=strip|keep
--addprefix=PREFIX
--addoldprefix=PREFIX
--addnewprefix=PREFIX
@@ -2291,8 +2358,10 @@ will pipe patch of file #2 to vim - -R
- You can use both unified and context format diffs with
- this program.
+ You can use unified, context, and Git format diffs with
+ this program. Git format includes support for binary files,
+ file renames, permission mode changes, and other Git-specific
+ diff features.
@@ -2342,6 +2411,23 @@ will pipe patch of file #2 to vim - -R
+
+ =strip|keep
+
+ How to handle a/ and b/
+ prefixes in Git diff filenames. With strip, removes
+ the prefixes both for filename matching (when using
+ and options) and for filename output (similar to
+ ). With keep (default),
+ preserves existing behavior. Applies to both Git-specific diffs (binary files,
+ renames, mode changes) and traditional diffs when part of a Git patch.
+ Note: Git rename operations use rename from/to headers
+ that contain literal filenames without a/ or
+ b/ prefixes, so this option does not affect renamed
+ filenames. The default will change to strip in version 0.5.0.
+
+
+
=PREFIX
@@ -2603,6 +2689,20 @@ will pipe patch of file #2 to vim - -R
+
+ Examples
+
+ Git format diffs are fully supported. For example, to find
+ files in a git patch that contain changes to malloc calls:
+
+
+
+ Note that grepdiff searches in the hunk content (the actual
+ code changes), not in Git metadata like rename headers. Files with
+ only renames or mode changes (no content hunks) won't be found
+ even if the pattern appears in the Git headers.
+
+
See also
diff --git a/src/diff.c b/src/diff.c
index 2406623d..c0bcea84 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -939,7 +939,7 @@ static FILE *do_convert (FILE *f, const char *mode, int seekable,
error (EXIT_FAILURE, errno, "fdopen failed");
if (seekable) {
- FILE *tmp = xtmpfile ();
+ FILE *tmp = xtmpfile ();
while (!feof (ret)) {
int c = fgetc (ret);
@@ -1089,6 +1089,18 @@ read_timestamp (const char *timestamp, struct tm *result, long *zone)
return 0;
}
+/* Helper function to strip Git a/ or b/ prefixes from a filename */
+static char *
+strip_git_prefix_from_filename (const char *filename, enum git_prefix_mode prefix_mode)
+{
+ if (prefix_mode == GIT_PREFIX_STRIP &&
+ ((filename[0] == 'a' && filename[1] == '/') ||
+ (filename[0] == 'b' && filename[1] == '/'))) {
+ return xstrdup (filename + 2);
+ }
+ return xstrdup (filename);
+}
+
char *
filename_from_header (const char *header)
{
@@ -1112,3 +1124,146 @@ filename_from_header (const char *header)
return xstrndup (header, h);
}
+
+char *
+filename_from_header_with_git_prefix_mode (const char *header, enum git_prefix_mode prefix_mode)
+{
+ char *filename = filename_from_header (header);
+ if (prefix_mode == GIT_PREFIX_STRIP) {
+ char *stripped = strip_git_prefix_from_filename (filename, prefix_mode);
+ free (filename);
+ return stripped;
+ }
+ return filename;
+}
+
+/* Detect the type of git diff based on headers.
+ * Returns GIT_DIFF_NORMAL for non-git diffs or regular git diffs with hunks,
+ * or a specific git diff type for special cases like renames, copies, etc.
+ */
+enum git_diff_type
+detect_git_diff_type (char **headers, unsigned int num_headers)
+{
+ unsigned int i;
+ int has_similarity_100 = 0;
+ int has_rename = 0;
+ int has_copy = 0;
+ int has_binary = 0;
+ int has_mode_change = 0;
+ int has_new_file = 0;
+ int has_deleted_file = 0;
+
+ /* Check if this is even a git diff format.
+ * If not, return GIT_DIFF_NORMAL (the default for regular diffs) */
+ if (num_headers == 0 || strncmp (headers[0], "diff --git ", 11))
+ return GIT_DIFF_NORMAL;
+
+ /* Analyze the headers */
+ for (i = 0; i < num_headers; i++) {
+ if (!strncmp (headers[i], "similarity index 100%", 21))
+ has_similarity_100 = 1;
+ else if (!strncmp (headers[i], "rename from ", 12) ||
+ !strncmp (headers[i], "rename to ", 10))
+ has_rename = 1;
+ else if (!strncmp (headers[i], "copy from ", 10) ||
+ !strncmp (headers[i], "copy to ", 8))
+ has_copy = 1;
+ else if (!strncmp (headers[i], "old mode ", 9) ||
+ !strncmp (headers[i], "new mode ", 9))
+ has_mode_change = 1;
+ else if (!strncmp (headers[i], "new file mode ", 14))
+ has_new_file = 1;
+ else if (!strncmp (headers[i], "deleted file mode ", 18))
+ has_deleted_file = 1;
+ else if (strstr (headers[i], "Binary files ") ||
+ !strncmp (headers[i], "GIT binary patch", 16))
+ has_binary = 1;
+ }
+
+ /* Determine the type based on header combinations */
+ if (has_similarity_100 && has_rename)
+ return GIT_DIFF_RENAME;
+ else if (has_copy)
+ return GIT_DIFF_COPY;
+ else if (has_binary)
+ return GIT_DIFF_BINARY;
+ else if (has_mode_change && !has_new_file && !has_deleted_file)
+ return GIT_DIFF_MODE_ONLY;
+ else if (has_new_file)
+ return GIT_DIFF_NEW_FILE;
+ else if (has_deleted_file)
+ return GIT_DIFF_DELETED_FILE;
+ else
+ /* Regular git diff with hunks (no special operations) */
+ return GIT_DIFF_NORMAL;
+}
+
+int
+extract_git_filenames (char **headers, unsigned int num_headers,
+ char **old_name, char **new_name, enum git_prefix_mode prefix_mode)
+{
+ unsigned int i;
+ char *git_line = NULL;
+ char *rename_from = NULL, *rename_to = NULL;
+
+ *old_name = NULL;
+ *new_name = NULL;
+
+ /* Find the diff --git line */
+ for (i = 0; i < num_headers; i++) {
+ if (!strncmp (headers[i], "diff --git ", 11)) {
+ git_line = headers[i];
+ break;
+ }
+ }
+
+ if (!git_line)
+ return 1; /* Not a git diff */
+
+ /* Look for rename/copy headers first */
+ for (i = 0; i < num_headers; i++) {
+ if (!strncmp (headers[i], "rename from ", 12))
+ rename_from = headers[i] + 12;
+ else if (!strncmp (headers[i], "rename to ", 10))
+ rename_to = headers[i] + 10;
+ else if (!strncmp (headers[i], "copy from ", 10))
+ rename_from = headers[i] + 10;
+ else if (!strncmp (headers[i], "copy to ", 8))
+ rename_to = headers[i] + 8;
+ }
+
+ if (rename_from && rename_to) {
+ /* Use rename headers for filenames */
+ *old_name = xstrndup (rename_from, strcspn (rename_from, "\n\r"));
+ *new_name = xstrndup (rename_to, strcspn (rename_to, "\n\r"));
+ } else {
+ /* Parse filenames from "diff --git a/path b/path" line */
+ char *p = git_line + 11; /* Skip "diff --git " */
+ char *space1, *space2;
+
+ /* Find the space between a/path and b/path */
+ space1 = strchr (p, ' ');
+ if (!space1)
+ return 1;
+
+ /* Extract old filename, conditionally stripping a/ prefix */
+ if (prefix_mode == GIT_PREFIX_STRIP && space1 - p > 2 && p[0] == 'a' && p[1] == '/') {
+ *old_name = xstrndup (p + 2, space1 - p - 2);
+ } else {
+ *old_name = xstrndup (p, space1 - p);
+ }
+
+ /* Find start of new filename (b/path) */
+ space2 = space1 + 1;
+
+ /* Extract new filename, conditionally stripping b/ prefix */
+ size_t new_len = strcspn (space2, "\n\r");
+ if (prefix_mode == GIT_PREFIX_STRIP && new_len > 2 && space2[0] == 'b' && space2[1] == '/') {
+ *new_name = xstrndup (space2 + 2, new_len - 2);
+ } else {
+ *new_name = xstrndup (space2, new_len);
+ }
+ }
+
+ return 0;
+}
diff --git a/src/diff.h b/src/diff.h
index f047c960..9ff694bb 100644
--- a/src/diff.h
+++ b/src/diff.h
@@ -54,4 +54,25 @@ int read_timestamp (const char *timestamp,
struct tm *result /* may be NULL */,
long *zone /* may be NULL */);
+/* Git diff support */
+enum git_diff_type {
+ GIT_DIFF_NORMAL = 0, /* Regular diff with hunks */
+ GIT_DIFF_RENAME, /* Pure rename (similarity index 100%) */
+ GIT_DIFF_COPY, /* File copy (similarity < 100%) */
+ GIT_DIFF_BINARY, /* Binary file diff */
+ GIT_DIFF_MODE_ONLY, /* Mode change only */
+ GIT_DIFF_NEW_FILE, /* New file creation */
+ GIT_DIFF_DELETED_FILE /* File deletion */
+};
+
+enum git_prefix_mode {
+ GIT_PREFIX_KEEP, /* default for compatibility */
+ GIT_PREFIX_STRIP
+};
+
char *filename_from_header (const char *header);
+char *filename_from_header_with_git_prefix_mode (const char *header, enum git_prefix_mode prefix_mode);
+
+enum git_diff_type detect_git_diff_type (char **headers, unsigned int num_headers);
+int extract_git_filenames (char **headers, unsigned int num_headers,
+ char **old_name, char **new_name, enum git_prefix_mode prefix_mode);
diff --git a/src/filterdiff.c b/src/filterdiff.c
index d0a77408..a6087ea4 100644
--- a/src/filterdiff.c
+++ b/src/filterdiff.c
@@ -110,6 +110,21 @@ static int print_patchnames = -1;
static int empty_files_as_absent = 0;
static unsigned long filecount=0;
+static enum git_prefix_mode git_prefix_mode = GIT_PREFIX_KEEP;
+
+/* Helper function to check if current patch is a Git patch */
+static int
+is_git_patch (char **headers, unsigned int num_headers)
+{
+ unsigned int i;
+ for (i = 0; i < num_headers; i++) {
+ if (!strncmp (headers[i], "diff --git ", 11)) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
static int
regexecs (regex_t *regex, size_t num_regex, const char *string,
size_t nmatch, regmatch_t pmatch[], int eflags)
@@ -171,8 +186,8 @@ static int output_header_line (const char *line)
char *fn;
if (strncmp (line, "diff", 4) == 0 && isspace (line[4])) {
- size_t args = 0;
- const char *end = line + 5, *begin = end, *ws = end;
+ size_t args = 0;
+ const char *end = line + 5, *begin = end, *ws = end;
printf ("%.5s", line);
while (*end != 0) {
if (isspace (*begin))
@@ -329,9 +344,105 @@ hunk_matches (unsigned long orig_offset, unsigned long orig_count,
return 1;
}
+static int
+do_git_diff_no_hunks (FILE *f, char **header, unsigned int num_headers,
+ int match, char **line, size_t *linelen,
+ unsigned long *linenum, unsigned long start_linenum,
+ char status, const char *bestname, const char *patchname,
+ int *orig_file_exists, int *new_file_exists,
+ enum git_diff_type git_type)
+{
+ unsigned int i;
+ int displayed_filename = 0;
+
+ /* Set file existence based on git diff type */
+ switch (git_type) {
+ case GIT_DIFF_NEW_FILE:
+ *orig_file_exists = 0;
+ *new_file_exists = 1;
+ break;
+ case GIT_DIFF_DELETED_FILE:
+ *orig_file_exists = 1;
+ *new_file_exists = 0;
+ break;
+ case GIT_DIFF_RENAME:
+ case GIT_DIFF_COPY:
+ case GIT_DIFF_BINARY:
+ case GIT_DIFF_MODE_ONLY:
+ default:
+ *orig_file_exists = 1;
+ *new_file_exists = 1;
+ break;
+ }
+
+ /* If this diff matches the filter, display it */
+ if (match) {
+ if (mode == mode_filter) {
+ /* Output all headers for this git diff */
+ for (i = 0; i < num_headers; i++)
+ fputs (header[i], stdout);
+ } else if (mode == mode_list && !displayed_filename) {
+ if (!show_status) {
+ display_filename (start_linenum, status,
+ bestname, patchname);
+ }
+ displayed_filename = 1;
+ }
+ }
+
+ /* Look for any additional content after headers (like binary patches)
+ * Note: we might already have a content line in *line if we broke out of header reading */
+ int first_iteration = 1;
+ while (first_iteration || getline (line, linelen, f) != -1) {
+ if (!first_iteration)
+ ++*linenum;
+ first_iteration = 0;
+
+ /* Check if we've hit the next diff */
+ if (!strncmp (*line, "diff ", 5))
+ return 1; /* Found next diff, return to main loop */
+
+ /* Check for binary patch content */
+ if (!strncmp (*line, "GIT binary patch", 16) ||
+ !strncmp (*line, "literal ", 8) ||
+ !strncmp (*line, "delta ", 6)) {
+ /* Output binary content if this diff matches */
+ if (match && mode == mode_filter) {
+ fputs (*line, stdout);
+ /* Continue reading binary content until next diff or EOF */
+ while (getline (line, linelen, f) != -1) {
+ ++*linenum;
+ if (!strncmp (*line, "diff ", 5))
+ return 1;
+ fputs (*line, stdout);
+ /* Binary patches end with empty line typically */
+ if ((*line)[0] == '\n')
+ break;
+ }
+ } else {
+ /* Skip binary content if not matching */
+ while (getline (line, linelen, f) != -1) {
+ ++*linenum;
+ if (!strncmp (*line, "diff ", 5))
+ return 1;
+ if ((*line)[0] == '\n')
+ break;
+ }
+ }
+ break;
+ }
+
+ /* For other types of content, just output if matching */
+ if (match && mode == mode_filter)
+ fputs (*line, stdout);
+ }
+
+ return 0; /* EOF reached */
+}
+
static int
do_unified (FILE *f, char **header, unsigned int num_headers,
- int match, char **line,
+ int match, char **line,
size_t *linelen, unsigned long *linenum,
unsigned long start_linenum, char status,
const char *bestname, const char *patchname,
@@ -368,8 +479,15 @@ do_unified (FILE *f, char **header, unsigned int num_headers,
if (!orig_count && !new_count && **line != '\\') {
char *trailing;
- if (strncmp (*line, "@@ ", 3))
+ if (strncmp (*line, "@@ ", 3)) {
+ /* Check if this is the start of the next diff */
+ if (!strncmp (*line, "diff ", 5) ||
+ !strncmp (*line, "--- ", 4) ||
+ !strncmp (*line, "*** ", 4)) {
+ ret = 1; /* Found next diff */
+ }
break;
+ }
/* Next chunk. */
hunknum++;
@@ -442,9 +560,9 @@ do_unified (FILE *f, char **header, unsigned int num_headers,
if (!header_displayed &&
mode != mode_grep) {
// Display the header.
- unsigned int i;
- for (i = 0; i < num_headers - 2; i++)
- output_header_line (header[i]);
+ unsigned int i;
+ for (i = 0; i < num_headers - 2; i++)
+ output_header_line (header[i]);
if (number_lines != After && number_lines != OriginalAfter)
output_header_line (header[num_headers - 2]);
if (number_lines != Before && number_lines != OriginalBefore)
@@ -556,16 +674,16 @@ do_unified (FILE *f, char **header, unsigned int num_headers,
}
} else {
if (match_tmpf) {
- if (!header_displayed) {
- unsigned int i;
- for (i = 0; i < num_headers - 2; i++)
- output_header_line (header[i]);
- if (number_lines != After && number_lines != OriginalAfter)
- output_header_line (header[num_headers - 2]);
- if (number_lines != Before && number_lines != OriginalBefore)
- output_header_line (header[num_headers - 1]);
+ if (!header_displayed) {
+ unsigned int i;
+ for (i = 0; i < num_headers - 2; i++)
+ output_header_line (header[i]);
+ if (number_lines != After && number_lines != OriginalAfter)
+ output_header_line (header[num_headers - 2]);
+ if (number_lines != Before && number_lines != OriginalBefore)
+ output_header_line (header[num_headers - 1]);
header_displayed = 1;
- }
+ }
rewind (match_tmpf);
while (!feof (match_tmpf)) {
@@ -595,14 +713,14 @@ do_unified (FILE *f, char **header, unsigned int num_headers,
(number_lines == OriginalBefore && **line != '+') ||
(number_lines == OriginalAfter && **line != '-')) {
// Numbered line.
- const char *rest = *line;
- if (rest[0] != '\n')
- // Handle whitespace damage
- rest++;
+ const char *rest = *line;
+ if (rest[0] != '\n')
+ // Handle whitespace damage
+ rest++;
fprintf (output_to, "%lu\t:%s",
track_linenum++, rest);
- }
+ }
}
}
@@ -622,7 +740,7 @@ do_unified (FILE *f, char **header, unsigned int num_headers,
static int
do_context (FILE *f, char **header, unsigned int num_headers,
- int match, char **line,
+ int match, char **line,
size_t *linelen, unsigned long *linenum,
unsigned long start_linenum, char status,
const char *bestname, const char *patchname,
@@ -776,9 +894,9 @@ do_context (FILE *f, char **header, unsigned int num_headers,
// Display the line counts.
if (!header_displayed && mode == mode_filter) {
- unsigned int i;
- for (i = 0; i < num_headers - 2; i++)
- output_header_line (header[i]);
+ unsigned int i;
+ for (i = 0; i < num_headers - 2; i++)
+ output_header_line (header[i]);
if (number_lines != After && number_lines != OriginalAfter)
output_header_line (header[num_headers - 2]);
if (number_lines != Before && number_lines != OriginalBefore)
@@ -907,9 +1025,9 @@ do_context (FILE *f, char **header, unsigned int num_headers,
}
} else {
if (!header_displayed) {
- unsigned int i;
- for (i = 0; i < num_headers - 2; i++)
- output_header_line (header[i]);
+ unsigned int i;
+ for (i = 0; i < num_headers - 2; i++)
+ output_header_line (header[i]);
if (number_lines != After && number_lines != OriginalAfter)
output_header_line (header[num_headers - 2]);
if (number_lines != Before && number_lines != OriginalBefore)
@@ -1030,7 +1148,7 @@ static int filterdiff (FILE *f, const char *patchname)
static unsigned long linenum = 1;
char *names[2];
char *header[MAX_HEADERS + 2] = { NULL, NULL };
- unsigned int num_headers = 0;
+ unsigned int num_headers = 0;
char *line = NULL;
size_t linelen = 0;
char *p;
@@ -1046,9 +1164,10 @@ static int filterdiff (FILE *f, const char *patchname)
unsigned long start_linenum;
int orig_file_exists, new_file_exists;
int is_context = -1;
+ int is_git_diff = 0; /* Flag to track if we're processing a git diff */
int result;
int (*do_diff) (FILE *, char **, unsigned int,
- int, char **, size_t *,
+ int, char **, size_t *,
unsigned long *, unsigned long,
char, const char *, const char *,
int *, int *);
@@ -1058,8 +1177,8 @@ static int filterdiff (FILE *f, const char *patchname)
// Search for start of patch ("diff ", or "--- " for
// unified diff, "*** " for context).
for (;;) {
- if (!strncmp (line, "diff ", 5))
- break;
+ if (!strncmp (line, "diff ", 5))
+ break;
if (!strncmp (line, "--- ", 4)) {
is_context = 0;
@@ -1084,73 +1203,210 @@ static int filterdiff (FILE *f, const char *patchname)
start_linenum = linenum;
header[0] = xstrdup (line);
- num_headers = 1;
-
- if (is_context == -1) {
- int valid_extended = 1;
- for (;;) {
- if (getline (&line, &linelen, f) == -1)
- goto eof;
- linenum++;
-
- if (!strncmp (line, "diff ", 5)) {
- header[num_headers++] = xstrdup (line);
- break;
- }
-
- if (!strncmp (line, "--- ", 4))
- is_context = 0;
- else if (!strncmp (line, "*** ", 4))
- is_context = 1;
- else if (strncmp (line, "old mode ", 9) &&
- strncmp (line, "new mode ", 9) &&
- strncmp (line, "deleted file mode ", 18) &&
- strncmp (line, "new file mode ", 14) &&
- strncmp (line, "copy from ", 10) &&
- strncmp (line, "copy to ", 8) &&
- strncmp (line, "rename from ", 12) &&
- strncmp (line, "rename to ", 10) &&
- strncmp (line, "similarity index ", 17) &&
- strncmp (line, "dissimilarity index ", 20) &&
- strncmp (line, "index ", 6))
- valid_extended = 0;
-
- if (!valid_extended)
- break;
-
- /* Drop excess header lines */
- if (num_headers > MAX_HEADERS )
- free (header[--num_headers]);
-
- header[num_headers++] = xstrdup (line);
-
- if (is_context != -1)
- break;
- }
-
- if (!valid_extended)
- goto flush_continue;
- }
-
- if (is_context == -1) {
- /* We don't yet do anything with diffs with
- * zero hunks. */
- unsigned int i = 0;
- flush_continue:
- if (mode == mode_filter && (pat_exclude || verbose)
- && !clean_comments) {
- for (i = 0; i < num_headers; i++)
- fputs (header[i], stdout);
- }
- for (i = 0; i < num_headers; i++) {
- free (header[i]);
- header[i] = NULL;
- }
- num_headers = 0;
- continue;
- }
-
- names[0] = filename_from_header (line + 4);
+ num_headers = 1;
+
+ if (is_context == -1) {
+ int valid_extended = 1;
+ int hit_eof = 0;
+ for (;;) {
+ if (getline (&line, &linelen, f) == -1) {
+ hit_eof = 1;
+ break;
+ }
+ linenum++;
+
+ if (!strncmp (line, "diff ", 5)) {
+ /* Found start of next diff - don't add to headers */
+ break;
+ }
+
+ if (!strncmp (line, "--- ", 4))
+ is_context = 0;
+ else if (!strncmp (line, "*** ", 4))
+ is_context = 1;
+ else if (!strncmp (line, "Binary files ", 13)) {
+ /* Binary files line - this is diff content, break to process */
+ break;
+ } else if (strncmp (line, "old mode ", 9) &&
+ strncmp (line, "new mode ", 9) &&
+ strncmp (line, "deleted file mode ", 18) &&
+ strncmp (line, "new file mode ", 14) &&
+ strncmp (line, "copy from ", 10) &&
+ strncmp (line, "copy to ", 8) &&
+ strncmp (line, "rename from ", 12) &&
+ strncmp (line, "rename to ", 10) &&
+ strncmp (line, "similarity index ", 17) &&
+ strncmp (line, "dissimilarity index ", 20) &&
+ strncmp (line, "index ", 6))
+ valid_extended = 0;
+
+ if (!valid_extended)
+ break;
+
+ /* Drop excess header lines */
+ if (num_headers > MAX_HEADERS )
+ free (header[--num_headers]);
+
+ header[num_headers++] = xstrdup (line);
+
+ if (is_context != -1)
+ break;
+ }
+
+ if (!valid_extended)
+ goto flush_continue;
+
+ /* If we hit EOF while reading extended headers, but have valid git headers,
+ * we should still process this as a git diff without hunks */
+ if (hit_eof && valid_extended && is_context == -1) {
+ /* Process as git diff without hunks and then exit */
+ enum git_diff_type git_type = detect_git_diff_type (header, num_headers);
+
+ if (git_type != GIT_DIFF_NORMAL) {
+ char *git_old_name = NULL, *git_new_name = NULL;
+ const char *p_stripped;
+ int match;
+
+ is_git_diff = 1; /* Mark that we're processing a git diff */
+
+ /* Extract filenames from git headers */
+ if (extract_git_filenames (header, num_headers,
+ &git_old_name, &git_new_name, git_prefix_mode) == 0) {
+ /* Use the best name for filtering */
+ char *names[2] = { git_old_name, git_new_name };
+ p = best_name (2, names);
+ p_stripped = stripped (p, ignore_components);
+
+ /* Apply include/exclude filters */
+ match = !patlist_match(pat_exclude, p_stripped);
+ if (match && pat_include != NULL)
+ match = patlist_match(pat_include, p_stripped);
+
+ /* Print filename if in list mode and matches */
+ if (match && !show_status && mode == mode_list)
+ display_filename (start_linenum, status, p, patchname);
+
+ /* Output headers if matching and in filter mode */
+ if (match && mode == mode_filter) {
+ unsigned int i;
+ for (i = 0; i < num_headers; i++)
+ fputs (header[i], stdout);
+ }
+
+ /* Set file existence based on git diff type */
+ switch (git_type) {
+ case GIT_DIFF_NEW_FILE:
+ orig_file_exists = 0;
+ new_file_exists = 1;
+ break;
+ case GIT_DIFF_DELETED_FILE:
+ orig_file_exists = 1;
+ new_file_exists = 0;
+ break;
+ default:
+ orig_file_exists = 1;
+ new_file_exists = 1;
+ break;
+ }
+
+ /* Print filename with status if in list mode and matches */
+ if (match && show_status && mode == mode_list) {
+ if (!orig_file_exists)
+ status = '+';
+ else if (!new_file_exists)
+ status = '-';
+
+ display_filename (start_linenum, status, p, patchname);
+ }
+
+ /* Clean up */
+ free (git_old_name);
+ free (git_new_name);
+ }
+ }
+ goto eof;
+ }
+ }
+
+ if (is_context == -1) {
+ /* Check if this is a git diff without hunks or with content like Binary files */
+ enum git_diff_type git_type = detect_git_diff_type (header, num_headers);
+
+ if (git_type != GIT_DIFF_NORMAL) {
+ /* This is a git diff without hunks - handle it */
+ char *git_old_name = NULL, *git_new_name = NULL;
+ const char *p_stripped;
+ int match;
+ int result;
+
+ is_git_diff = 1; /* Mark that we're processing a git diff */
+
+ /* Extract filenames from git headers */
+ if (extract_git_filenames (header, num_headers,
+ &git_old_name, &git_new_name, git_prefix_mode)) {
+ /* Fallback to traditional method if git extraction fails */
+ goto flush_continue;
+ }
+
+ /* Use the best name for filtering */
+ char *names[2] = { git_old_name, git_new_name };
+ p = best_name (2, names);
+ p_stripped = stripped (p, ignore_components);
+
+ /* Apply include/exclude filters */
+ match = !patlist_match(pat_exclude, p_stripped);
+ if (match && pat_include != NULL)
+ match = patlist_match(pat_include, p_stripped);
+
+ /* Process the git diff (it will handle filename display) */
+ result = do_git_diff_no_hunks (f, header, num_headers,
+ match, &line, &linelen, &linenum,
+ start_linenum, status, p, patchname,
+ &orig_file_exists, &new_file_exists,
+ git_type);
+
+ /* Print filename with status if in list mode and matches */
+ if (match && show_status && mode == mode_list) {
+ if (!orig_file_exists)
+ status = '+';
+ else if (!new_file_exists)
+ status = '-';
+
+ display_filename (start_linenum, status, p, patchname);
+ }
+
+ /* Clean up */
+ free (git_old_name);
+ free (git_new_name);
+
+ /* Handle result */
+ if (result == EOF)
+ goto eof;
+
+ goto next_diff;
+ } else {
+ /* Not a git diff without hunks - use original logic */
+ unsigned int i = 0;
+ flush_continue:
+ if (mode == mode_filter && (pat_exclude || verbose)
+ && !clean_comments) {
+ for (i = 0; i < num_headers; i++)
+ fputs (header[i], stdout);
+ }
+ for (i = 0; i < num_headers; i++) {
+ free (header[i]);
+ header[i] = NULL;
+ }
+ num_headers = 0;
+ continue;
+ }
+ }
+
+ if (is_git_patch (header, num_headers)) {
+ names[0] = filename_from_header_with_git_prefix_mode (line + 4, git_prefix_mode);
+ } else {
+ names[0] = filename_from_header (line + 4);
+ }
if (mode != mode_filter && show_status)
orig_file_exists = file_exists (names[0], line + 4 +
strlen (names[0]));
@@ -1170,12 +1426,16 @@ static int filterdiff (FILE *f, const char *patchname)
/* Show non-diff lines if excluding, or if
* in verbose mode, and if --clean isn't specified. */
free (names[0]);
- goto flush_continue;
+ goto flush_continue;
}
filecount++;
header[num_headers++] = xstrdup (line);
- names[1] = filename_from_header (line + 4);
+ if (is_git_patch (header, num_headers)) {
+ names[1] = filename_from_header_with_git_prefix_mode (line + 4, git_prefix_mode);
+ } else {
+ names[1] = filename_from_header (line + 4);
+ }
if (mode != mode_filter && show_status)
new_file_exists = file_exists (names[1], line + 4 +
@@ -1200,7 +1460,7 @@ static int filterdiff (FILE *f, const char *patchname)
do_diff = do_unified;
result = do_diff (f, header, num_headers,
- match, &line,
+ match, &line,
&linelen, &linenum,
start_linenum, status, p, patchname,
&orig_file_exists, &new_file_exists);
@@ -1222,17 +1482,29 @@ static int filterdiff (FILE *f, const char *patchname)
free (names[1]);
goto eof;
case 1:
- goto next_diff;
+ /* Found next diff - line variable contains the start of next diff */
+ free (names[0]);
+ free (names[1]);
+ for (i = 0; i < num_headers; i++) {
+ free (header[i]);
+ header[i] = NULL;
+ }
+ num_headers = 0;
+ /* Continue processing with the current line (don't call getline) */
+ continue;
}
next_diff:
- for (i = 0; i < 2; i++)
- free (names[i]);
- for (i = 0; i < num_headers; i++) {
+ /* Only free names if we're not processing a git diff (which handles its own cleanup) */
+ if (!is_git_diff) {
+ for (i = 0; i < 2; i++)
+ free (names[i]);
+ }
+ for (i = 0; i < num_headers; i++) {
free (header[i]);
header[i] = NULL;
}
- num_headers = 0;
+ num_headers = 0;
}
eof:
@@ -1292,6 +1564,9 @@ const char * syntax_str =
" -p N, --strip-match=N\n"
" initial pathname components to ignore\n"
" --strip=N initial pathname components to strip\n"
+" --git-prefixes=strip|keep\n"
+" how to handle a/ and b/ prefixes in Git diffs for both filename\n"
+" matching (-i/-x) and output (default: keep)\n"
" --addprefix=PREFIX\n"
" prefix pathnames with PREFIX\n"
" --addoldprefix=PREFIX\n"
@@ -1512,7 +1787,7 @@ int main (int argc, char *argv[])
determine_mode_from_name (argv[0]);
while (1) {
static struct option long_options[] = {
- {"help", 0, 0, 1000 + 'H'},
+ {"help", 0, 0, 1000 + 'H'},
{"version", 0, 0, 1000 + 'V'},
{"verbose", 0, 0, 'v'},
{"list", 0, 0, 'l'},
@@ -1549,6 +1824,7 @@ int main (int argc, char *argv[])
{"empty-files-as-removed", 0, 0, 'E'},
{"file", 1, 0, 'f'},
{"in-place", 0, 0, 1000 + 'w'},
+ {"git-prefixes", 1, 0, 1000 + 'G'},
{0, 0, 0, 0}
};
char *end;
@@ -1707,13 +1983,13 @@ int main (int argc, char *argv[])
if (!strncmp (optarg, "all", 3))
only_matching = only_match_all;
else if (!strncmp (optarg, "rem", 3) ||
- !strncmp (optarg, "removals", 8))
+ !strncmp (optarg, "removals", 8))
only_matching = only_match_rem;
else if (!strncmp (optarg, "add", 3) ||
- !strncmp (optarg, "additions", 9))
+ !strncmp (optarg, "additions", 9))
only_matching = only_match_add;
else if (!strncmp (optarg, "mod", 3) ||
- !strncmp (optarg, "modifications", 13))
+ !strncmp (optarg, "modifications", 13))
only_matching = only_match_mod;
else syntax (1);
break;
@@ -1726,13 +2002,22 @@ int main (int argc, char *argv[])
case 1000 + 'w':
inplace_mode = 1;
break;
+ case 1000 + 'G':
+ if (!strcmp(optarg, "strip")) {
+ git_prefix_mode = GIT_PREFIX_STRIP;
+ } else if (!strcmp(optarg, "keep")) {
+ git_prefix_mode = GIT_PREFIX_KEEP;
+ } else {
+ error(EXIT_FAILURE, 0, "invalid argument to --git-prefixes: %s (expected 'strip' or 'keep')", optarg);
+ }
+ break;
default:
syntax(1);
}
}
if (have_switches == 0 && strcmp (progname, "patchview") == 0) {
- mode = mode_list;
- number_files = 1;
+ mode = mode_list;
+ number_files = 1;
}
/* Preserve the old semantics of -p. */
diff --git a/tests/fullheader1/run-test b/tests/fullheader1/run-test
index bfc37625..258877ce 100755
--- a/tests/fullheader1/run-test
+++ b/tests/fullheader1/run-test
@@ -22,14 +22,14 @@ index 5feceb9..dde7898 10644
+be selected
EOF
-${LSDIFF} -n git-output 2>errors >index || exit 1
+${LSDIFF} --git-prefixes=strip -n git-output 2>errors >index || exit 1
[ -s errors ] && exit 1
cat <<"EOF" | cmp - index || exit 1
-1 a/ChangeLog
-8 a/otherfile
+1 ChangeLog
+8 otherfile
EOF
-${FILTERDIFF} -p1 -i otherfile git-output 2>errors >otherfile.patch || exit 1
+${FILTERDIFF} --git-prefixes=strip -i otherfile git-output 2>errors >otherfile.patch || exit 1
[ -s errors ] && exit 1
cat <<"EOF" | cmp - otherfile.patch || exit 1
diff --git a/otherfile b/otherfile
diff --git a/tests/git-binary-formats/run-test b/tests/git-binary-formats/run-test
new file mode 100755
index 00000000..7b76dd3b
--- /dev/null
+++ b/tests/git-binary-formats/run-test
@@ -0,0 +1,126 @@
+#!/bin/sh
+
+# Test coverage for binary patch content handling
+# Tests binary patch formats: "GIT binary patch" and "literal"
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: GIT binary patch format
+cat << 'EOF' > git-binary-patch.patch
+diff --git a/file1.bin b/file1.bin
+new file mode 100644
+index 0000000..abc123
+Binary files /dev/null and b/file1.bin differ
+GIT binary patch
+literal 12
+TcmZn~00001
+
+diff --git a/file2.txt b/file2.txt
+new file mode 100644
+index 0000000..def456
+--- /dev/null
++++ b/file2.txt
+@@ -0,0 +1 @@
++text content
+EOF
+
+# Test 2: literal format binary patch
+cat << 'EOF' > literal-patch.patch
+diff --git a/file3.bin b/file3.bin
+new file mode 100644
+index 0000000..ghi789
+Binary files /dev/null and b/file3.bin differ
+literal 8
+PcmZn~0001
+
+diff --git a/file4.txt b/file4.txt
+new file mode 100644
+index 0000000..jkl012
+--- /dev/null
++++ b/file4.txt
+@@ -0,0 +1 @@
++more text
+EOF
+
+# Test 3: delta format binary patch (working format)
+cat << 'EOF' > delta-patch.patch
+diff --git a/file5.bin b/file5.bin
+new file mode 100644
+index 0000000..mno345
+Binary files /dev/null and b/file5.bin differ
+delta 16
+XcmZn~00002Tc
+
+diff --git a/file6.txt b/file6.txt
+new file mode 100644
+index 0000000..pqr678
+--- /dev/null
++++ b/file6.txt
+@@ -0,0 +1 @@
++final text
+EOF
+
+# Test filtering binary files with GIT binary patch format - should include binary content
+echo "Testing GIT binary patch format (include)..."
+${FILTERDIFF} --git-prefixes=strip -i "file1.bin" git-binary-patch.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && { echo "Unexpected errors in test 1:"; cat errors1; exit 1; }
+
+cat << 'EOF' | cmp - result1 || { echo "Test 1 failed"; exit 1; }
+diff --git a/file1.bin b/file1.bin
+new file mode 100644
+index 0000000..abc123
+Binary files /dev/null and b/file1.bin differ
+GIT binary patch
+literal 12
+TcmZn~00001
+
+EOF
+
+# Test filtering binary files with literal format - should include binary content
+echo "Testing literal format (include)..."
+${FILTERDIFF} --git-prefixes=strip -i "file3.bin" literal-patch.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && { echo "Unexpected errors in test 2:"; cat errors2; exit 1; }
+
+cat << 'EOF' | cmp - result2 || { echo "Test 2 failed"; exit 1; }
+diff --git a/file3.bin b/file3.bin
+new file mode 100644
+index 0000000..ghi789
+Binary files /dev/null and b/file3.bin differ
+literal 8
+PcmZn~0001
+
+EOF
+
+# Test filtering binary files with delta format - should include binary content
+echo "Testing delta format (include)..."
+${FILTERDIFF} --git-prefixes=strip -i "file5.bin" delta-patch.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && { echo "Unexpected errors in test 3:"; cat errors3; exit 1; }
+
+cat << 'EOF' | cmp - result3 || { echo "Test 3 failed"; exit 1; }
+diff --git a/file5.bin b/file5.bin
+new file mode 100644
+index 0000000..mno345
+Binary files /dev/null and b/file5.bin differ
+delta 16
+XcmZn~00002Tc
+
+EOF
+
+# Test excluding binary files - should skip binary content
+echo "Testing binary patch exclusion..."
+${FILTERDIFF} --git-prefixes=strip -x "file1.bin" git-binary-patch.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && { echo "Unexpected errors in test 4:"; cat errors4; exit 1; }
+
+cat << 'EOF' | cmp - result4 || { echo "Test 4 failed"; exit 1; }
+
+diff --git a/file2.txt b/file2.txt
+new file mode 100644
+index 0000000..def456
+--- /dev/null
++++ b/file2.txt
+@@ -0,0 +1 @@
++text content
+EOF
+
+echo "All binary format tests passed!"
+exit 0
diff --git a/tests/git-binary-issue57/run-test b/tests/git-binary-issue57/run-test
new file mode 100755
index 00000000..bdc99fe7
--- /dev/null
+++ b/tests/git-binary-issue57/run-test
@@ -0,0 +1,58 @@
+#!/bin/sh
+
+# Test for GitHub Issue #57: Binary files not supported
+# This test ensures that binary file diffs are handled correctly
+
+. ${top_srcdir-.}/tests/common.sh
+
+cat << EOF > git-binary.patch
+diff --git a/binary-file b/binary-file
+new file mode 100644
+index 0000000..998a95d
+Binary files /dev/null and b/binary-file differ
+diff --git a/text-file.txt b/text-file.txt
+new file mode 100644
+index 0000000..abcdefg
+--- /dev/null
++++ b/text-file.txt
+@@ -0,0 +1,2 @@
++line 1
++line 2
+EOF
+
+# Test that filterdiff includes binary files when they match
+${FILTERDIFF} --git-prefixes=strip -i binary-file git-binary.patch 2>errors >result || exit 1
+[ -s errors ] && exit 1
+
+cat << EOF | cmp - result || exit 1
+diff --git a/binary-file b/binary-file
+new file mode 100644
+index 0000000..998a95d
+Binary files /dev/null and b/binary-file differ
+EOF
+
+# Test that lsdiff shows binary files
+${LSDIFF} --git-prefixes=strip git-binary.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
+binary-file
+text-file.txt
+EOF
+
+# Test excluding binary files
+${FILTERDIFF} --git-prefixes=strip -x binary-file git-binary.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+cat << EOF | cmp - result3 || exit 1
+diff --git a/text-file.txt b/text-file.txt
+new file mode 100644
+index 0000000..abcdefg
+--- /dev/null
++++ b/text-file.txt
+@@ -0,0 +1,2 @@
++line 1
++line 2
+EOF
+
+exit 0
diff --git a/tests/git-complex-mixed/run-test b/tests/git-complex-mixed/run-test
new file mode 100755
index 00000000..3eec2e6c
--- /dev/null
+++ b/tests/git-complex-mixed/run-test
@@ -0,0 +1,138 @@
+#!/bin/sh
+
+# Test complex mixed git diff scenarios
+# This test covers multiple git diff types in one patch file and edge cases
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Create a complex patch with multiple git diff types:
+# 1. New file with content
+# 2. Deleted file
+# 3. Binary file
+# 4. Mode-only change
+# 5. Pure rename (100% similarity)
+# 6. Copy operation (< 100% similarity)
+# 7. Regular content change
+cat << EOF > complex-git.patch
+diff --git a/new-file.txt b/new-file.txt
+new file mode 100644
+index 0000000..abcdef1
+--- /dev/null
++++ b/new-file.txt
+@@ -0,0 +1,2 @@
++This is a new file
++with some content
+diff --git a/deleted-file.old b/deleted-file.old
+deleted file mode 100644
+index fedcba9..0000000
+--- a/deleted-file.old
++++ /dev/null
+@@ -1,2 +0,0 @@
+-This file will be deleted
+-goodbye
+diff --git a/binary-data.bin b/binary-data.bin
+new file mode 100644
+index 0000000..1234567
+Binary files /dev/null and b/binary-data.bin differ
+diff --git a/script-mode.sh b/script-mode.sh
+old mode 100644
+new mode 100755
+diff --git a/renamed-file.txt b/new-name.txt
+similarity index 100%
+rename from renamed-file.txt
+rename to new-name.txt
+diff --git a/copied-file.c b/copy-target.c
+similarity index 85%
+copy from copied-file.c
+copy to copy-target.c
+index abc123..def456 100644
+--- a/copied-file.c
++++ b/copy-target.c
+@@ -1,3 +1,4 @@
+ #include
+ int main() {
++ printf("copied and modified\\n");
+ return 0;
+ }
+diff --git a/regular-change.h b/regular-change.h
+index 111222..333444 100644
+--- a/regular-change.h
++++ b/regular-change.h
+@@ -1,2 +1,2 @@
+-#define OLD_VERSION 1
++#define NEW_VERSION 2
+ /* header file */
+EOF
+
+# Test 1: lsdiff should list all files correctly
+${LSDIFF} complex-git.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+cat << EOF | cmp - result1 || exit 1
+b/new-file.txt
+a/deleted-file.old
+a/binary-data.bin
+a/script-mode.sh
+new-name.txt
+a/copied-file.c
+a/regular-change.h
+EOF
+
+# Test 2: lsdiff with --git-prefixes=strip
+${LSDIFF} --git-prefixes=strip complex-git.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
+new-file.txt
+deleted-file.old
+binary-data.bin
+script-mode.sh
+new-name.txt
+copied-file.c
+regular-change.h
+EOF
+
+# Test 3: Filter only new files
+${FILTERDIFF} -i "*.txt" complex-git.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+# Should include new-file.txt and new-name.txt (renamed file)
+grep -q "new-file.txt" result3 || exit 1
+grep -q "new-name.txt" result3 || exit 1
+grep -q "binary-data.bin" result3 && exit 1 # Should not include binary
+
+# Test 4: Filter binary files specifically
+${FILTERDIFF} -i "*.bin" complex-git.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+cat << EOF | cmp - result4 || exit 1
+diff --git a/binary-data.bin b/binary-data.bin
+new file mode 100644
+index 0000000..1234567
+Binary files /dev/null and b/binary-data.bin differ
+EOF
+
+# Test 5: Exclude deleted files
+${FILTERDIFF} -x "*.old" complex-git.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+# Should not contain deleted-file.old
+grep -q "deleted-file.old" result5 && exit 1
+
+# Test 6: Test grepdiff on content changes only
+${GREPDIFF} "NEW_VERSION" complex-git.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+
+# Should only match regular-change.h
+grep -q "regular-change.h" result6 || exit 1
+grep -q "new-file.txt" result6 && exit 1 # Should not match files without the pattern
+
+# Test 7: Test with -p strip on complex paths
+${FILTERDIFF} --git-prefixes=strip -p0 --strip=0 complex-git.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+
+# Should preserve all content
+grep -q "new-file.txt" result7 || exit 1
+grep -q "binary-data.bin" result7 || exit 1
+
+exit 0
diff --git a/tests/git-copy-operations/run-test b/tests/git-copy-operations/run-test
new file mode 100755
index 00000000..d12f761c
--- /dev/null
+++ b/tests/git-copy-operations/run-test
@@ -0,0 +1,198 @@
+#!/bin/sh
+
+# Test git copy operations (similarity < 100%)
+# This test covers file copies with modifications
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: Basic copy operation with modifications
+cat << EOF > copy-basic.patch
+diff --git a/original.c b/copy1.c
+similarity index 85%
+copy from original.c
+copy to copy1.c
+index abc123..def456 100644
+--- a/original.c
++++ b/copy1.c
+@@ -1,5 +1,6 @@
+ #include
+
+ int main() {
++ printf("This is a copy\\n");
+ return 0;
+ }
+EOF
+
+# Test lsdiff with copy operation
+${LSDIFF} copy-basic.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+cat << EOF | cmp - result1 || exit 1
+b/copy1.c
+EOF
+
+# Test filterdiff with copy operation
+${FILTERDIFF} -i "b/copy1.c" copy-basic.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
+diff --git a/original.c b/copy1.c
+similarity index 85%
+copy from original.c
+copy to copy1.c
+index abc123..def456 100644
+--- a/original.c
++++ b/copy1.c
+@@ -1,5 +1,6 @@
+ #include
+
+ int main() {
++ printf("This is a copy\\n");
+ return 0;
+ }
+EOF
+
+# Test 2: Multiple copy operations in one patch
+cat << EOF > copy-multiple.patch
+diff --git a/base.h b/copy1.h
+similarity index 90%
+copy from base.h
+copy to copy1.h
+index 111222..333444 100644
+--- a/base.h
++++ b/copy1.h
+@@ -1,3 +1,4 @@
+ #ifndef BASE_H
+ #define BASE_H
++#define COPY1_VERSION 1
+ #endif
+diff --git a/base.h b/copy2.h
+similarity index 80%
+copy from base.h
+copy to copy2.h
+index 111222..555666 100644
+--- a/base.h
++++ b/copy2.h
+@@ -1,3 +1,5 @@
+ #ifndef BASE_H
+ #define BASE_H
++#define COPY2_VERSION 2
++#define EXTRA_FEATURE
+ #endif
+EOF
+
+# Test lsdiff with multiple copies
+${LSDIFF} copy-multiple.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+cat << EOF | cmp - result3 || exit 1
+a/base.h
+a/base.h
+EOF
+
+# Test filtering by source file (matches both copies)
+${FILTERDIFF} -i "a/base.h" copy-multiple.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+grep -q "copy2.h" result4 || exit 1
+grep -q "copy1.h" result4 || exit 1
+
+# Test 3: Copy with --git-prefixes=strip
+${LSDIFF} --git-prefixes=strip copy-multiple.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+cat << EOF | cmp - result5 || exit 1
+base.h
+base.h
+EOF
+
+# Test 4: Copy operation with binary file
+cat << EOF > copy-binary.patch
+diff --git a/data.bin b/backup.bin
+similarity index 100%
+copy from data.bin
+copy to backup.bin
+Binary files a/data.bin and b/backup.bin differ
+EOF
+
+${LSDIFF} copy-binary.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+
+cat << EOF | cmp - result6 || exit 1
+data.bin
+EOF
+
+${FILTERDIFF} -i "data.bin" copy-binary.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+
+cat << EOF | cmp - result7 || exit 1
+diff --git a/data.bin b/backup.bin
+similarity index 100%
+copy from data.bin
+copy to backup.bin
+Binary files a/data.bin and b/backup.bin differ
+EOF
+
+# Test 5: Mixed copy and rename operations
+cat << EOF > copy-rename-mixed.patch
+diff --git a/file1.txt b/file1-copy.txt
+similarity index 75%
+copy from file1.txt
+copy to file1-copy.txt
+index abc123..def456 100644
+--- a/file1.txt
++++ b/file1-copy.txt
+@@ -1,2 +1,3 @@
+ original content
++copied and modified
+ end
+diff --git a/file2.txt b/file2-renamed.txt
+similarity index 100%
+rename from file2.txt
+rename to file2-renamed.txt
+EOF
+
+${LSDIFF} copy-rename-mixed.patch 2>errors8 >result8 || exit 1
+[ -s errors8 ] && exit 1
+
+cat << EOF | cmp - result8 || exit 1
+a/file1.txt
+file2.txt
+EOF
+
+# Test 6: Copy with very low similarity
+cat << EOF > copy-low-similarity.patch
+diff --git a/template.c b/heavily-modified.c
+similarity index 15%
+copy from template.c
+copy to heavily-modified.c
+index abc123..xyz789 100644
+--- a/template.c
++++ b/heavily-modified.c
+@@ -1,10 +1,20 @@
+ #include
++#include
++#include
++#include
+
+-int main() {
+- return 0;
++int main(int argc, char **argv) {
++ if (argc < 2) {
++ fprintf(stderr, "Usage: %s \\n", argv[0]);
++ return 1;
++ }
++
++ printf("Processing: %s\\n", argv[1]);
++ // Heavy modifications here
++ return 0;
+ }
+EOF
+
+${FILTERDIFF} copy-low-similarity.patch 2>errors9 >result9 || exit 1
+[ -s errors9 ] && exit 1
+
+grep -q "heavily-modified.c" result9 || exit 1
+grep -q "similarity index 15%" result9 || exit 1
+
+exit 0
diff --git a/tests/git-deleted-file/run-test b/tests/git-deleted-file/run-test
new file mode 100755
index 00000000..4bdc95f8
--- /dev/null
+++ b/tests/git-deleted-file/run-test
@@ -0,0 +1,130 @@
+#!/bin/sh
+
+# Test for GIT_DIFF_DELETED_FILE functionality
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: Basic deleted file
+cat << 'EOF' > git-deleted.patch
+diff --git a/deleted-file.txt b/deleted-file.txt
+deleted file mode 100644
+index abc123..0000000
+--- a/deleted-file.txt
++++ /dev/null
+@@ -1,3 +0,0 @@
+-line 1
+-line 2
+-line 3
+EOF
+
+# Test 2: Deleted file without hunks (pure header-only deletion)
+cat << 'EOF' > git-deleted-no-hunks.patch
+diff --git a/removed.c b/removed.c
+deleted file mode 100644
+index def456..0000000
+Binary files a/removed.c and /dev/null differ
+EOF
+
+# Test 3: Multiple deleted files in one patch
+cat << 'EOF' > git-multiple-deleted.patch
+diff --git a/file1.txt b/file1.txt
+deleted file mode 100644
+index 111..0000000
+--- a/file1.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-content1
+diff --git a/file2.txt b/file2.txt
+deleted file mode 100644
+index 222..0000000
+--- a/file2.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-content2
+EOF
+
+echo "=== Test 1: lsdiff with deleted file ==="
+${LSDIFF} --git-prefixes=strip -s git-deleted.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+# Should show file with '-' status
+cat << EOF | cmp - result1 || exit 1
+- deleted-file.txt
+EOF
+
+echo "=== Test 2: filterdiff include deleted file ==="
+${FILTERDIFF} --git-prefixes=strip -i "deleted*" git-deleted.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+# Should include the full diff
+cat << 'EOF' | cmp - result2 || exit 1
+diff --git a/deleted-file.txt b/deleted-file.txt
+deleted file mode 100644
+index abc123..0000000
+--- a/deleted-file.txt
++++ /dev/null
+@@ -1,3 +0,0 @@
+-line 1
+-line 2
+-line 3
+EOF
+
+echo "=== Test 3: filterdiff exclude deleted file ==="
+${FILTERDIFF} --git-prefixes=strip -x "deleted*" git-deleted.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+# Should be empty
+[ -s result3 ] && exit 1
+
+echo "=== Test 4: lsdiff with deleted file (no hunks) ==="
+${LSDIFF} --git-prefixes=strip -s git-deleted-no-hunks.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+cat << EOF | cmp - result4 || exit 1
+- removed.c
+EOF
+
+echo "=== Test 5: filterdiff with deleted file (no hunks) ==="
+${FILTERDIFF} --git-prefixes=strip -i "*.c" git-deleted-no-hunks.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+cat << 'EOF' | cmp - result5 || exit 1
+diff --git a/removed.c b/removed.c
+deleted file mode 100644
+index def456..0000000
+Binary files a/removed.c and /dev/null differ
+EOF
+
+echo "=== Test 6: lsdiff with multiple deleted files ==="
+${LSDIFF} --git-prefixes=strip -s git-multiple-deleted.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+
+cat << EOF | cmp - result6 || exit 1
+- file1.txt
+- file2.txt
+EOF
+
+echo "=== Test 7: filterdiff with pattern matching multiple deleted files ==="
+${FILTERDIFF} --git-prefixes=strip -i "file1*" git-multiple-deleted.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+
+cat << 'EOF' | cmp - result7 || exit 1
+diff --git a/file1.txt b/file1.txt
+deleted file mode 100644
+index 111..0000000
+--- a/file1.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-content1
+EOF
+
+echo "=== Test 8: Test git-prefixes with deleted files ==="
+${LSDIFF} --git-prefixes=strip -s git-deleted.patch 2>errors8 >result8 || exit 1
+[ -s errors8 ] && exit 1
+
+cat << EOF | cmp - result8 || exit 1
+- deleted-file.txt
+EOF
+
+echo "All deleted file tests passed!"
+exit 0
diff --git a/tests/git-diff-duplication/run-test b/tests/git-diff-duplication/run-test
new file mode 100755
index 00000000..b41cf651
--- /dev/null
+++ b/tests/git-diff-duplication/run-test
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+# This is a filterdiff(1) testcase.
+# Test: Bug where filterdiff duplicates "diff --git" lines
+# When processing multiple git diff blocks where the second block has both
+# rename and content changes, filterdiff incorrectly duplicates the second
+# "diff --git" line. The output should be unchanged.
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Create a minimal test case that triggers the duplication bug
+cat << EOF > git-diff-bug.patch
+diff --git a/file1 b/file2
+similarity index 100%
+rename from file1
+rename to file2
+diff --git a/file3 b/file4
+similarity index 98%
+rename from file3
+rename to file4
+index 1111111..2222222 100644
+--- a/file3
++++ b/file4
+@@ -1 +1 @@
+-old
++new
+EOF
+
+# Test that filterdiff preserves the output unchanged
+${FILTERDIFF} --git-prefixes=strip git-diff-bug.patch 2>errors >result || exit 1
+[ -s errors ] && exit 1
+
+# The result should be identical to the input
+cmp git-diff-bug.patch result || exit 1
+
+exit 0
diff --git a/tests/git-diff-edge-cases/run-test b/tests/git-diff-edge-cases/run-test
new file mode 100755
index 00000000..abceb6da
--- /dev/null
+++ b/tests/git-diff-edge-cases/run-test
@@ -0,0 +1,220 @@
+#!/bin/sh
+
+# Test for edge cases and error conditions in git diff processing
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: Mode-only change (GIT_DIFF_MODE_ONLY)
+cat << 'EOF' > git-mode-only.patch
+diff --git a/script.sh b/script.sh
+old mode 100644
+new mode 100755
+index abc123..abc123 100644
+EOF
+
+# Test 2: Git diff with no hunks and binary marker
+cat << 'EOF' > git-binary-no-hunks.patch
+diff --git a/image.png b/image.png
+new file mode 100644
+index 0000000..abc123
+Binary files /dev/null and b/image.png differ
+EOF
+
+# Test 3: Copy operation (not pure rename)
+cat << 'EOF' > git-copy.patch
+diff --git a/original.txt b/copy.txt
+similarity index 85%
+copy from original.txt
+copy to copy.txt
+index abc123..def456 100644
+--- a/original.txt
++++ b/copy.txt
+@@ -1,3 +1,4 @@
+ line 1
+ line 2
+ line 3
++added line
+EOF
+
+# Test 4: Malformed git headers (should fall back gracefully)
+cat << 'EOF' > malformed-git.patch
+diff --git incomplete
+some random content
+--- a/file.txt
++++ b/file.txt
+@@ -1 +1 @@
+-old
++new
+EOF
+
+# Test 5: Git diff with both old and new mode changes
+cat << 'EOF' > git-mode-and-content.patch
+diff --git a/file.txt b/file.txt
+old mode 100644
+new mode 100755
+index abc123..def456
+--- a/file.txt
++++ b/file.txt
+@@ -1 +1,2 @@
+ content
++new line
+EOF
+
+# Test 6: Empty git diff (no changes)
+cat << 'EOF' > git-empty.patch
+diff --git a/unchanged.txt b/unchanged.txt
+index abc123..abc123 100644
+EOF
+
+echo "=== Test 1: Mode-only change ==="
+${LSDIFF} -s git-mode-only.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+cat << EOF | cmp - result1 || exit 1
+! a/script.sh
+EOF
+
+echo "=== Test 2: Binary file with no hunks ==="
+${LSDIFF} -s git-binary-no-hunks.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
++ a/image.png
+EOF
+
+echo "=== Test 3: Copy operation ==="
+${LSDIFF} -s git-copy.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+cat << EOF | cmp - result3 || exit 1
+! b/copy.txt
+EOF
+
+echo "=== Test 4: filterdiff with copy operation ==="
+${FILTERDIFF} -i "*/copy*" git-copy.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+cat << 'EOF' | cmp - result4 || exit 1
+diff --git a/original.txt b/copy.txt
+similarity index 85%
+copy from original.txt
+copy to copy.txt
+index abc123..def456 100644
+--- a/original.txt
++++ b/copy.txt
+@@ -1,3 +1,4 @@
+ line 1
+ line 2
+ line 3
++added line
+EOF
+
+echo "=== Test 5: filterdiff with copy by target name ==="
+${FILTERDIFF} -i "*/copy*" git-copy.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+# Should still match (uses best_name)
+cat << 'EOF' | cmp - result5 || exit 1
+diff --git a/original.txt b/copy.txt
+similarity index 85%
+copy from original.txt
+copy to copy.txt
+index abc123..def456 100644
+--- a/original.txt
++++ b/copy.txt
+@@ -1,3 +1,4 @@
+ line 1
+ line 2
+ line 3
++added line
+EOF
+
+echo "=== Test 6: Malformed git headers fallback ==="
+${LSDIFF} malformed-git.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+
+cat << EOF | cmp - result6 || exit 1
+a/file.txt
+EOF
+
+echo "=== Test 7: Mode and content change ==="
+${LSDIFF} -s git-mode-and-content.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+
+cat << EOF | cmp - result7 || exit 1
+! a/file.txt
+EOF
+
+echo "=== Test 8: Empty git diff ==="
+${LSDIFF} -s git-empty.patch 2>errors8 >result8 || exit 1
+[ -s errors8 ] && exit 1
+
+# Empty git diff should produce no output
+[ -s result8 ] && exit 1
+
+echo "=== Test 9: filterdiff with git-prefixes on various types ==="
+${FILTERDIFF} --git-prefixes=strip -i "*.sh" git-mode-only.patch 2>errors9 >result9 || exit 1
+[ -s errors9 ] && exit 1
+
+cat << 'EOF' | cmp - result9 || exit 1
+diff --git a/script.sh b/script.sh
+old mode 100644
+new mode 100755
+index abc123..abc123 100644
+EOF
+
+echo "=== Test 10: Test include/exclude with no matches ==="
+${FILTERDIFF} -i "nonexistent*" git-mode-only.patch 2>errors10 >result10 || exit 1
+[ -s errors10 ] && exit 1
+
+# Should be empty
+[ -s result10 ] && exit 1
+
+echo "=== Test 11: Multiple git diff types in one patch ==="
+cat << 'EOF' > mixed-git-types.patch
+diff --git a/deleted.txt b/deleted.txt
+deleted file mode 100644
+index abc123..0000000
+--- a/deleted.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-content
+diff --git a/old.txt b/new.txt
+similarity index 100%
+rename from old.txt
+rename to new.txt
+diff --git a/script.sh b/script.sh
+old mode 100644
+new mode 100755
+index def456..def456 100644
+EOF
+
+${LSDIFF} -s mixed-git-types.patch 2>errors11 >result11 || exit 1
+[ -s errors11 ] && exit 1
+
+cat << EOF | cmp - result11 || exit 1
+- a/deleted.txt
+! old.txt
+! a/script.sh
+EOF
+
+echo "=== Test 12: filterdiff with pattern matching mixed types ==="
+${FILTERDIFF} -i "*.txt" mixed-git-types.patch 2>errors12 >result12 || exit 1
+[ -s errors12 ] && exit 1
+
+cat << 'EOF' | cmp - result12 || exit 1
+diff --git a/deleted.txt b/deleted.txt
+deleted file mode 100644
+index abc123..0000000
+--- a/deleted.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-content
+diff --git a/old.txt b/new.txt
+similarity index 100%
+rename from old.txt
+rename to new.txt
+EOF
+
+echo "All edge case tests passed!"
+exit 0
diff --git a/tests/git-error-handling/run-test b/tests/git-error-handling/run-test
new file mode 100755
index 00000000..0fbe3bfb
--- /dev/null
+++ b/tests/git-error-handling/run-test
@@ -0,0 +1,149 @@
+#!/bin/sh
+
+# Test error handling and edge cases for git diff processing
+# This test ensures robust handling of malformed or unusual git diffs
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: Malformed git diff line (missing space)
+cat << EOF > malformed1.patch
+diff --gita/file.txt b/file.txt
+index abc123..def456 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1 +1 @@
+-old
++new
+EOF
+
+# Should handle gracefully and treat as regular diff
+${FILTERDIFF} malformed1.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+grep -q "file.txt" result1 || exit 1
+
+# Test 2: Git diff with missing filenames in git line
+cat << EOF > malformed2.patch
+diff --git
+index abc123..def456 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1 +1 @@
+-old
++new
+EOF
+
+# Should handle gracefully
+${FILTERDIFF} malformed2.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+# Test 3: Git diff with only one filename in git line
+cat << EOF > malformed3.patch
+diff --git a/single-file.txt
+index abc123..def456 100644
+--- a/single-file.txt
++++ b/single-file.txt
+@@ -1 +1 @@
+-old
++new
+EOF
+
+# Should handle gracefully
+${FILTERDIFF} malformed3.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+# Test 4: Git diff with unusual prefixes (not a/ b/)
+cat << EOF > unusual-prefixes.patch
+diff --git x/file.txt y/file.txt
+index abc123..def456 100644
+--- x/file.txt
++++ y/file.txt
+@@ -1 +1 @@
+-old
++new
+EOF
+
+# Should handle gracefully
+${FILTERDIFF} unusual-prefixes.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+grep -q "file.txt" result4 || exit 1
+
+# Test 5: Test --git-prefixes with unusual prefixes
+${FILTERDIFF} --git-prefixes=strip unusual-prefixes.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+grep -q "file.txt" result5 || exit 1
+
+# Test 6: Git diff with no prefixes at all
+cat << EOF > no-prefixes.patch
+diff --git file.txt file.txt
+index abc123..def456 100644
+--- file.txt
++++ file.txt
+@@ -1 +1 @@
+-old
++new
+EOF
+
+${FILTERDIFF} --git-prefixes=strip no-prefixes.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+grep -q "file.txt" result6 || exit 1
+
+# Test 7: Binary diff with "Binary files" format (standard)
+cat << EOF > git-binary-patch.patch
+diff --git a/binary.dat b/binary.dat
+new file mode 100644
+index 0000000..1234567
+Binary files /dev/null and b/binary.dat differ
+EOF
+
+${FILTERDIFF} -i "*binary*" git-binary-patch.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+grep -q "binary.dat" result7 || exit 1
+
+# Test 8: Test lsdiff with binary patch format
+${LSDIFF} git-binary-patch.patch 2>errors8 >result8 || exit 1
+[ -s errors8 ] && exit 1
+
+cat << EOF | cmp - result8 || exit 1
+a/binary.dat
+EOF
+
+# Test 9: Mixed binary formats in one patch
+cat << EOF > mixed-binary.patch
+diff --git a/file1.bin b/file1.bin
+new file mode 100644
+index 0000000..abc123
+Binary files /dev/null and b/file1.bin differ
+diff --git a/file2.dat b/file2.dat
+new file mode 100644
+index 0000000..def456
+GIT binary patch
+literal 512
+zcmV+00000000000000000000000000000000000000000000000000000000
+M000000000000000000000000000000000000000000000000000000000000
+literal 0
+HcmV?d00001
+
+EOF
+
+${LSDIFF} mixed-binary.patch 2>errors9 >result9 || exit 1
+[ -s errors9 ] && exit 1
+
+cat << EOF | cmp - result9 || exit 1
+a/file1.bin
+EOF
+
+# Test 10: Empty git diff (just headers, no content)
+cat << EOF > empty-git.patch
+diff --git a/empty.txt b/empty.txt
+new file mode 100644
+index 0000000..e69de29
+--- /dev/null
++++ b/empty.txt
+EOF
+
+${FILTERDIFF} empty-git.patch 2>errors10 >result10 || exit 1
+[ -s errors10 ] && exit 1
+# Empty patch should produce no output
+[ ! -s result10 ] || exit 1
+
+exit 0
diff --git a/tests/git-exclude-issue27/run-test b/tests/git-exclude-issue27/run-test
new file mode 100755
index 00000000..f8ec26f4
--- /dev/null
+++ b/tests/git-exclude-issue27/run-test
@@ -0,0 +1,48 @@
+#!/bin/sh
+
+# Test for GitHub Issue #27: -x $file only removes the diff body from git diff output
+# This test ensures that when excluding files, no orphaned git headers remain
+
+. ${top_srcdir-.}/tests/common.sh
+
+cat << EOF > git-multi.patch
+diff --git a/README.vin b/README.vin
+index d7a1ea2..7c298d7 100644
+--- a/README.vin
++++ b/README.vin
+@@ -1,2 +1,2 @@
+-old content
++new content
+ some line
+diff --git a/autogen.sh b/autogen.sh
+index 1234567..abcdefg 100644
+--- a/autogen.sh
++++ b/autogen.sh
+@@ -1,2 +1,2 @@
+-old script
++new script
+EOF
+
+# Test that excluding README.vin removes the entire git diff block
+${FILTERDIFF} --git-prefixes=strip -x README.vin git-multi.patch 2>errors >result || exit 1
+[ -s errors ] && exit 1
+
+# The result should contain the complete autogen.sh diff, no orphaned headers
+cat << EOF | cmp - result || exit 1
+diff --git a/autogen.sh b/autogen.sh
+index 1234567..abcdefg 100644
+--- a/autogen.sh
++++ b/autogen.sh
+@@ -1,2 +1,2 @@
+-old script
++new script
+EOF
+
+# Test that excluding both files results in empty output
+${FILTERDIFF} --git-prefixes=strip -x README.vin -x autogen.sh git-multi.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+# The result should be completely empty
+[ -s result2 ] && exit 1
+
+exit 0
diff --git a/tests/git-extended-headers/run-test b/tests/git-extended-headers/run-test
new file mode 100755
index 00000000..b012a5e6
--- /dev/null
+++ b/tests/git-extended-headers/run-test
@@ -0,0 +1,349 @@
+#!/bin/sh
+
+# Test all git extended headers for comprehensive coverage
+# This test ensures filterdiff correctly handles all git extended headers
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1a: deleted file mode (with hunks)
+cat << EOF > deleted-file.patch
+diff --git a/removed.txt b/removed.txt
+deleted file mode 100644
+index 1234567..0000000
+--- a/removed.txt
++++ /dev/null
+@@ -1,3 +0,0 @@
+-line 1
+-line 2
+-line 3
+EOF
+
+${LSDIFF} --git-prefixes=strip deleted-file.patch 2>errors1a >result1a || exit 1
+[ -s errors1a ] && exit 1
+
+cat << EOF | cmp - result1a || exit 1
+removed.txt
+EOF
+
+${FILTERDIFF} --git-prefixes=strip -i "removed.txt" deleted-file.patch 2>errors1a2 >result1a2 || exit 1
+[ -s errors1a2 ] && exit 1
+
+cat << EOF | cmp - result1a2 || exit 1
+diff --git a/removed.txt b/removed.txt
+deleted file mode 100644
+index 1234567..0000000
+--- a/removed.txt
++++ /dev/null
+@@ -1,3 +0,0 @@
+-line 1
+-line 2
+-line 3
+EOF
+
+# Test 1b: deleted file mode (no hunks - triggers do_git_diff_no_hunks)
+cat << EOF > deleted-file-no-hunks.patch
+diff --git a/empty-deleted.txt b/empty-deleted.txt
+deleted file mode 100644
+index e69de29..0000000
+EOF
+
+${LSDIFF} --git-prefixes=strip deleted-file-no-hunks.patch 2>errors1b >result1b || exit 1
+[ -s errors1b ] && exit 1
+
+cat << EOF | cmp - result1b || exit 1
+empty-deleted.txt
+EOF
+
+${FILTERDIFF} --git-prefixes=strip -i "empty-deleted.txt" deleted-file-no-hunks.patch 2>errors1b2 >result1b2 || exit 1
+[ -s errors1b2 ] && exit 1
+
+cat << EOF | cmp - result1b2 || exit 1
+diff --git a/empty-deleted.txt b/empty-deleted.txt
+deleted file mode 100644
+index e69de29..0000000
+EOF
+
+# Test 2a: new file mode (with hunks)
+cat << EOF > new-file.patch
+diff --git a/created.txt b/created.txt
+new file mode 100644
+index 0000000..abcdefg
+--- /dev/null
++++ b/created.txt
+@@ -0,0 +1,3 @@
++new line 1
++new line 2
++new line 3
+EOF
+
+${LSDIFF} --git-prefixes=strip new-file.patch 2>errors2a >result2a || exit 1
+[ -s errors2a ] && exit 1
+
+cat << EOF | cmp - result2a || exit 1
+created.txt
+EOF
+
+${FILTERDIFF} --git-prefixes=strip -i "created.txt" new-file.patch 2>errors2a2 >result2a2 || exit 1
+[ -s errors2a2 ] && exit 1
+
+cat << EOF | cmp - result2a2 || exit 1
+diff --git a/created.txt b/created.txt
+new file mode 100644
+index 0000000..abcdefg
+--- /dev/null
++++ b/created.txt
+@@ -0,0 +1,3 @@
++new line 1
++new line 2
++new line 3
+EOF
+
+# Test 2b: new file mode (no hunks - triggers do_git_diff_no_hunks)
+cat << EOF > new-file-no-hunks.patch
+diff --git a/empty-new.txt b/empty-new.txt
+new file mode 100644
+index 0000000..e69de29
+EOF
+
+${LSDIFF} --git-prefixes=strip new-file-no-hunks.patch 2>errors2b >result2b || exit 1
+[ -s errors2b ] && exit 1
+
+cat << EOF | cmp - result2b || exit 1
+empty-new.txt
+EOF
+
+${FILTERDIFF} --git-prefixes=strip -i "empty-new.txt" new-file-no-hunks.patch 2>errors2b2 >result2b2 || exit 1
+[ -s errors2b2 ] && exit 1
+
+cat << EOF | cmp - result2b2 || exit 1
+diff --git a/empty-new.txt b/empty-new.txt
+new file mode 100644
+index 0000000..e69de29
+EOF
+
+# Test 1c: deleted file with binary content (triggers do_git_diff_no_hunks path)
+cat << EOF > deleted-file-binary.patch
+diff --git a/pure-deleted.txt b/pure-deleted.txt
+deleted file mode 100644
+index e69de29..0000000
+Binary files a/pure-deleted.txt and /dev/null differ
+EOF
+
+${LSDIFF} --git-prefixes=strip deleted-file-binary.patch 2>errors1c >result1c || exit 1
+[ -s errors1c ] && exit 1
+
+cat << EOF | cmp - result1c || exit 1
+pure-deleted.txt
+EOF
+
+${FILTERDIFF} --git-prefixes=strip -i "pure-deleted.txt" deleted-file-binary.patch 2>errors1c2 >result1c2 || exit 1
+[ -s errors1c2 ] && exit 1
+
+cat << EOF | cmp - result1c2 || exit 1
+diff --git a/pure-deleted.txt b/pure-deleted.txt
+deleted file mode 100644
+index e69de29..0000000
+Binary files a/pure-deleted.txt and /dev/null differ
+EOF
+
+# Test 3: dissimilarity index (file rewrite)
+cat << EOF > dissimilar-file.patch
+diff --git a/rewritten.c b/rewritten.c
+dissimilarity index 95%
+index abc1234..def5678 100644
+--- a/rewritten.c
++++ b/rewritten.c
+@@ -1,10 +1,15 @@
+-#include
+-
+-int old_function() {
+- printf("old code");
+- return 0;
+-}
+-
+-int main() {
+- return old_function();
+-}
++#include
++#include
++
++struct new_data {
++ char *name;
++ int value;
++};
++
++int main(int argc, char **argv) {
++ struct new_data data;
++ data.name = strdup("new");
++ data.value = 42;
++ free(data.name);
++ return 0;
++}
+EOF
+
+${LSDIFF} --git-prefixes=strip dissimilar-file.patch 2>errors3a >result3a || exit 1
+[ -s errors3a ] && exit 1
+
+cat << EOF | cmp - result3a || exit 1
+rewritten.c
+EOF
+
+${FILTERDIFF} --git-prefixes=strip -i "rewritten.c" dissimilar-file.patch 2>errors3a2 >result3a2 || exit 1
+[ -s errors3a2 ] && exit 1
+
+grep -q "dissimilarity index 95%" result3a2 || exit 1
+grep -q "rewritten.c" result3a2 || exit 1
+
+# Test 4: Mixed extended headers in one patch
+cat << EOF > mixed-extended.patch
+diff --git a/old.txt b/old.txt
+deleted file mode 100644
+index abc123..0000000
+--- a/old.txt
++++ /dev/null
+@@ -1,2 +0,0 @@
+-old content
+-to be removed
+diff --git a/template.h b/variant1.h
+similarity index 60%
+copy from template.h
+copy to variant1.h
+index def456..ghi789 100644
+--- a/template.h
++++ b/variant1.h
+@@ -1,5 +1,6 @@
+ #ifndef TEMPLATE_H
+ #define TEMPLATE_H
++#define VARIANT1
+
+ void function();
+ #endif
+diff --git a/config.conf b/settings.conf
+similarity index 100%
+rename from config.conf
+rename to settings.conf
+diff --git a/script.sh b/script.sh
+old mode 100755
+new mode 100644
+index xyz987..uvw654 100644
+--- a/script.sh
++++ b/script.sh
+@@ -1,3 +1,3 @@
+ #!/bin/bash
+-echo "executable"
++echo "not executable"
+ exit 0
+diff --git a/brand-new.py b/brand-new.py
+new file mode 100755
+index 0000000..123abc
+--- /dev/null
++++ b/brand-new.py
+@@ -0,0 +1,4 @@
++#!/usr/bin/env python3
++
++if __name__ == "__main__":
++ print("Hello, World!")
+diff --git a/completely-rewritten.java b/completely-rewritten.java
+dissimilarity index 98%
+index old123..new456 100644
+--- a/completely-rewritten.java
++++ b/completely-rewritten.java
+@@ -1,15 +1,20 @@
+-public class OldClass {
+- private int oldField;
+-
+- public OldClass(int value) {
+- this.oldField = value;
+- }
+-
+- public void oldMethod() {
+- System.out.println("Old: " + oldField);
+- }
+-
+- public static void main(String[] args) {
+- new OldClass(42).oldMethod();
+- }
+-}
++import java.util.*;
++
++public class NewClass {
++ private Map data;
++
++ public NewClass() {
++ this.data = new HashMap<>();
++ }
++
++ public void addData(String key, Object value) {
++ data.put(key, value);
++ }
++
++ public void printAll() {
++ data.forEach((k, v) -> System.out.println(k + ": " + v));
++ }
++
++ public static void main(String[] args) {
++ new NewClass().printAll();
++ }
++}
+EOF
+
+# Test lsdiff shows all files
+${LSDIFF} --git-prefixes=strip mixed-extended.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+cat << EOF | cmp - result4 || exit 1
+old.txt
+template.h
+config.conf
+script.sh
+brand-new.py
+completely-rewritten.java
+EOF
+
+# Test filtering works with all header types
+${FILTERDIFF} --git-prefixes=strip -i "*.py" mixed-extended.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+grep -q "brand-new.py" result5 || exit 1
+grep -q "new file mode 100755" result5 || exit 1
+
+${FILTERDIFF} --git-prefixes=strip -i "*.java" mixed-extended.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+
+grep -q "completely-rewritten.java" result6 || exit 1
+grep -q "dissimilarity index 98%" result6 || exit 1
+
+${FILTERDIFF} --git-prefixes=strip -i "old.txt" mixed-extended.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+
+grep -q "deleted file mode 100644" result7 || exit 1
+
+# Test 5: Edge case - file with multiple extended headers
+cat << EOF > multiple-headers.patch
+diff --git a/complex.c b/renamed-complex.c
+similarity index 75%
+rename from complex.c
+rename to renamed-complex.c
+old mode 100644
+new mode 100755
+index abc123..def456 100755
+--- a/complex.c
++++ b/renamed-complex.c
+@@ -1,4 +1,5 @@
+ #include
++#include
+
+ int main() {
+ printf("Hello");
+EOF
+
+${FILTERDIFF} --git-prefixes=strip -i "*complex*.c" multiple-headers.patch 2>errors8 >result8 || exit 1
+[ -s errors8 ] && exit 1
+
+grep -q "similarity index 75%" result8 || exit 1
+grep -q "rename from complex.c" result8 || exit 1
+grep -q "rename to renamed-complex.c" result8 || exit 1
+grep -q "old mode 100644" result8 || exit 1
+grep -q "new mode 100755" result8 || exit 1
+
+exit 0
diff --git a/tests/git-lsdiff-complex/run-test b/tests/git-lsdiff-complex/run-test
new file mode 100755
index 00000000..ecc4572b
--- /dev/null
+++ b/tests/git-lsdiff-complex/run-test
@@ -0,0 +1,216 @@
+#!/bin/sh
+
+# Test complex git diff scenarios and edge cases for lsdiff -s
+# This test covers advanced combinations and mixed diff types
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: Complex mixed diff with all types
+cat << EOF > all-types-mixed.patch
+diff --git a/new-file.txt b/new-file.txt
+new file mode 100644
+index 0000000..abc123
+--- /dev/null
++++ b/new-file.txt
+@@ -0,0 +1 @@
++new content
+diff --git a/deleted-file.txt b/deleted-file.txt
+deleted file mode 100644
+index abc123..0000000
+--- a/deleted-file.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-deleted content
+diff --git a/old-name.c b/renamed.c
+similarity index 100%
+rename from old-name.c
+rename to renamed.c
+diff --git a/source.h b/copied.h
+similarity index 90%
+copy from source.h
+copy to copied.h
+index abc123..def456 100644
+--- a/source.h
++++ b/copied.h
+@@ -1 +1,2 @@
+ #ifndef HEADER_H
++#define COPIED_VERSION
+diff --git a/script.py b/script.py
+old mode 100644
+new mode 100755
+index abc123..abc123
+diff --git a/normal.c b/normal.c
+index abc123..def456 100644
+--- a/normal.c
++++ b/normal.c
+@@ -1 +1 @@
+-old line
++new line
+EOF
+
+${LSDIFF} -s all-types-mixed.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+cat << EOF | cmp - result1 || exit 1
++ b/new-file.txt
+- a/deleted-file.txt
+! renamed.c
+! b/copied.h
+! a/script.py
+! a/normal.c
+EOF
+
+# Test 2: Binary file operations (with proper git diff types)
+cat << EOF > binary-new-file.patch
+diff --git a/new-binary.bin b/new-binary.bin
+new file mode 100644
+index 0000000..abc123
+Binary files /dev/null and b/new-binary.bin differ
+EOF
+
+${LSDIFF} -s binary-new-file.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
++ a/new-binary.bin
+EOF
+
+# Test 3: Binary deleted file
+cat << EOF > binary-deleted-file.patch
+diff --git a/old-binary.bin b/old-binary.bin
+deleted file mode 100644
+index abc123..0000000
+Binary files a/old-binary.bin and /dev/null differ
+EOF
+
+${LSDIFF} -s binary-deleted-file.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+cat << EOF | cmp - result3 || exit 1
+- a/old-binary.bin
+EOF
+
+# Test 4: Binary file modification (should show output with proper headers)
+cat << EOF > binary-modified.patch
+diff --git a/data.bin b/data.bin
+index abc123..def456 100644
+GIT binary patch
+delta 123
+zcmV-;0E~(b0jh^H8Gi-<0001b0000000000000000000000000000000000
+delta 456
+zcmV-;0E~(b0jh^H8Gi-<0001b0000000000000000000000000000000000
+EOF
+
+${LSDIFF} -s binary-modified.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+cat << EOF | cmp - result4 || exit 1
+! a/data.bin
+EOF
+
+# Test 5: Mode change on new file (should be treated as new file)
+cat << EOF > new-file-with-mode.patch
+diff --git a/new-script.sh b/new-script.sh
+new file mode 100755
+index 0000000..abc123
+--- /dev/null
++++ b/new-script.sh
+@@ -0,0 +1,2 @@
++#!/bin/bash
++echo "hello"
+EOF
+
+${LSDIFF} -s new-file-with-mode.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+cat << EOF | cmp - result5 || exit 1
++ b/new-script.sh
+EOF
+
+# Test 6: Rename with very low similarity (should still be detected as rename)
+cat << EOF > rename-low-similarity.patch
+diff --git a/heavily-modified.c b/completely-different.c
+similarity index 15%
+rename from heavily-modified.c
+rename to completely-different.c
+index abc123..def456 100644
+--- a/heavily-modified.c
++++ b/completely-different.c
+@@ -1,10 +1,20 @@
+-// Old file
+-int old_function() {
+- return 0;
+-}
++// Completely rewritten
++#include
++#include
++
++int new_function(int arg) {
++ printf("New implementation: %d\n", arg);
++ return arg * 2;
++}
++
++int main() {
++ return new_function(42);
++}
+EOF
+
+${LSDIFF} -s rename-low-similarity.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+
+cat << EOF | cmp - result6 || exit 1
+! a/heavily-modified.c
+EOF
+
+# Test 7: Copy operation with 100% similarity
+cat << EOF > copy-identical.patch
+diff --git a/template.h b/instance.h
+similarity index 100%
+copy from template.h
+copy to instance.h
+index abc123..abc123 100644
+EOF
+
+${LSDIFF} -s copy-identical.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+
+cat << EOF | cmp - result7 || exit 1
+! instance.h
+EOF
+
+# Test 8: Multiple mode changes
+cat << EOF > multiple-modes.patch
+diff --git a/script1.sh b/script1.sh
+old mode 100644
+new mode 100755
+index abc123..abc123
+diff --git a/script2.py b/script2.py
+old mode 100755
+new mode 100644
+index def456..def456
+diff --git a/data.txt b/data.txt
+old mode 100644
+new mode 100644
+index 789abc..def789 100644
+--- a/data.txt
++++ b/data.txt
+@@ -1 +1 @@
+-old
++new
+EOF
+
+${LSDIFF} -s multiple-modes.patch 2>errors8 >result8 || exit 1
+[ -s errors8 ] && exit 1
+
+cat << EOF | cmp - result8 || exit 1
+! a/script1.sh
+! a/script2.py
+! a/data.txt
+EOF
+
+# Test 9: Test with filterdiff --list mode (should use same code paths)
+${FILTERDIFF} --list -s all-types-mixed.patch 2>errors9 >result9 || exit 1
+[ -s errors9 ] && exit 1
+
+# Should be identical to lsdiff -s output
+cmp result1 result9 || exit 1
diff --git a/tests/git-lsdiff-status/result5 b/tests/git-lsdiff-status/result5
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/git-lsdiff-status/run-test b/tests/git-lsdiff-status/run-test
new file mode 100755
index 00000000..d4117d2b
--- /dev/null
+++ b/tests/git-lsdiff-status/run-test
@@ -0,0 +1,154 @@
+#!/bin/sh
+
+# Test core git diff status handling with lsdiff -s
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: Git diff with new file (no hunks) - should show '+'
+cat << EOF > new-file-no-hunks.patch
+diff --git a/new-file.txt b/new-file.txt
+new file mode 100644
+index 0000000..abcdef1
+EOF
+
+${LSDIFF} -s new-file-no-hunks.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+cat << EOF | cmp - result1 || exit 1
++ a/new-file.txt
+EOF
+
+# Test 2: Git diff with deleted file (no hunks) - should show '-'
+cat << EOF > deleted-file-no-hunks.patch
+diff --git a/deleted-file.txt b/deleted-file.txt
+deleted file mode 100644
+index abcdef1..0000000
+EOF
+
+${LSDIFF} -s deleted-file-no-hunks.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
+- a/deleted-file.txt
+EOF
+
+# Test 3: Git diff with new file (with hunks) - should show '+'
+cat << EOF > new-file-with-hunks.patch
+diff --git a/new-file-hunks.txt b/new-file-hunks.txt
+new file mode 100644
+index 0000000..1234567
+--- /dev/null
++++ b/new-file-hunks.txt
+@@ -0,0 +1,3 @@
++line 1
++line 2
++line 3
+EOF
+
+${LSDIFF} -s new-file-with-hunks.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+cat << EOF | cmp - result3 || exit 1
++ b/new-file-hunks.txt
+EOF
+
+# Test 4: Git diff with deleted file (with hunks) - should show '-'
+cat << EOF > deleted-file-with-hunks.patch
+diff --git a/deleted-file-hunks.txt b/deleted-file-hunks.txt
+deleted file mode 100644
+index 1234567..0000000
+--- a/deleted-file-hunks.txt
++++ /dev/null
+@@ -1,3 +0,0 @@
+-line 1
+-line 2
+-line 3
+EOF
+
+${LSDIFF} -s deleted-file-with-hunks.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+cat << EOF | cmp - result4 || exit 1
+- a/deleted-file-hunks.txt
+EOF
+
+# Test 5: Malformed git diff with index changes but no hunks (not normally produced by git) - produces no output
+cat << EOF > modified-file-no-hunks.patch
+diff --git a/modified-file.txt b/modified-file.txt
+index abcdef1..1234567 100644
+EOF
+
+${LSDIFF} -s modified-file-no-hunks.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+# Should produce no output (empty file)
+[ ! -s result5 ] || exit 1
+
+# Test 6: Git diff with regular modification (with hunks) - should show '!'
+cat << EOF > modified-file-with-hunks.patch
+diff --git a/modified-file-hunks.txt b/modified-file-hunks.txt
+index abcdef1..1234567 100644
+--- a/modified-file-hunks.txt
++++ b/modified-file-hunks.txt
+@@ -1,3 +1,3 @@
+ line 1
+-old line
++new line
+ line 3
+EOF
+
+${LSDIFF} -s modified-file-with-hunks.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+
+cat << EOF | cmp - result6 || exit 1
+! a/modified-file-hunks.txt
+EOF
+
+# Test 7: Mixed git diff with multiple file types
+cat << EOF > mixed-git.patch
+diff --git a/new.txt b/new.txt
+new file mode 100644
+index 0000000..abc123
+--- /dev/null
++++ b/new.txt
+@@ -0,0 +1 @@
++new content
+diff --git a/deleted.txt b/deleted.txt
+deleted file mode 100644
+index def456..0000000
+--- a/deleted.txt
++++ /dev/null
+@@ -1 +0,0 @@
+-deleted content
+diff --git a/modified.txt b/modified.txt
+index ghi789..jkl012 100644
+--- a/modified.txt
++++ b/modified.txt
+@@ -1 +1 @@
+-old
++new
+diff --git a/empty-new.txt b/empty-new.txt
+new file mode 100644
+index 0000000..e69de29
+diff --git a/empty-deleted.txt b/empty-deleted.txt
+deleted file mode 100644
+index e69de29..0000000
+EOF
+
+${LSDIFF} -s mixed-git.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+
+cat << EOF | cmp - result7 || exit 1
++ b/new.txt
+- a/deleted.txt
+! a/modified.txt
++ a/empty-new.txt
+- a/empty-deleted.txt
+EOF
+
+# Test 8: Test with filterdiff in list mode (should use same code paths)
+${FILTERDIFF} --list -s mixed-git.patch 2>errors8 >result8 || exit 1
+[ -s errors8 ] && exit 1
+
+# Should be identical to lsdiff -s output
+cmp result7 result8 || exit 1
diff --git a/tests/git-mode-issue59/run-test b/tests/git-mode-issue59/run-test
new file mode 100755
index 00000000..46680e10
--- /dev/null
+++ b/tests/git-mode-issue59/run-test
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# Test for GitHub Issue #59: File permissions ignored
+# This test ensures that mode-only changes are handled correctly
+
+. ${top_srcdir-.}/tests/common.sh
+
+cat << EOF > git-mode.patch
+diff --git a/script.sh b/script.sh
+old mode 100755
+new mode 100644
+index abcdefg..1234567 100644
+--- a/script.sh
++++ b/script.sh
+@@ -1,3 +1,3 @@
+ #!/bin/bash
+-echo "old"
++echo "new"
+ exit 0
+diff --git a/mode-only.sh b/mode-only.sh
+old mode 100755
+new mode 100644
+EOF
+
+# Test that filterdiff includes mode-only changes when they match
+${FILTERDIFF} --git-prefixes=strip -i mode-only.sh git-mode.patch 2>errors >result || exit 1
+[ -s errors ] && exit 1
+
+cat << EOF | cmp - result || exit 1
+diff --git a/mode-only.sh b/mode-only.sh
+old mode 100755
+new mode 100644
+EOF
+
+# Test that lsdiff shows both files (one with content, one mode-only)
+${LSDIFF} --git-prefixes=strip git-mode.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
+script.sh
+mode-only.sh
+EOF
+
+# Test including files with content changes
+${FILTERDIFF} --git-prefixes=strip -i script.sh git-mode.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+cat << EOF | cmp - result3 || exit 1
+diff --git a/script.sh b/script.sh
+old mode 100755
+new mode 100644
+index abcdefg..1234567 100644
+--- a/script.sh
++++ b/script.sh
+@@ -1,3 +1,3 @@
+ #!/bin/bash
+-echo "old"
++echo "new"
+ exit 0
+EOF
+
+exit 0
diff --git a/tests/git-prefixes-option/run-test b/tests/git-prefixes-option/run-test
new file mode 100755
index 00000000..52b56b5d
--- /dev/null
+++ b/tests/git-prefixes-option/run-test
@@ -0,0 +1,111 @@
+#!/bin/sh
+
+# Test for --git-prefixes option
+# This test ensures that the --git-prefixes option works correctly
+# for both strip and keep modes with various types of Git diffs
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Create test patch with different types of Git diffs:
+# 1. Binary file (no hunks, uses diff --git line for filenames)
+# 2. File rename (uses "rename from/to" headers - no a/b/ prefixes)
+# 3. Mode-only change (no hunks, uses diff --git line for filenames)
+# 4. Regular file with content changes (has traditional --- +++ lines)
+cat << EOF > mixed-git.patch
+diff --git a/binary-file b/binary-file
+new file mode 100644
+index 0000000..998a95d
+Binary files /dev/null and b/binary-file differ
+diff --git a/renamed-file b/new-name
+similarity index 100%
+rename from renamed-file
+rename to new-name
+diff --git a/mode-only.sh b/mode-only.sh
+old mode 100755
+new mode 100644
+diff --git a/regular-file.txt b/regular-file.txt
+index abcdefg..1234567 100644
+--- a/regular-file.txt
++++ b/regular-file.txt
+@@ -1,2 +1,2 @@
+-old content
++new content
+EOF
+
+# Test default behavior (keep prefixes)
+${LSDIFF} mixed-git.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+# Expected output explanation:
+# - a/binary-file: from "diff --git a/binary-file b/binary-file" (keeps a/ prefix)
+# - new-name: from "rename to new-name" header (no prefix to keep - rename headers don't use a/b/ prefixes)
+# - a/mode-only.sh: from "diff --git a/mode-only.sh b/mode-only.sh" (keeps a/ prefix)
+# - a/regular-file.txt: from "diff --git a/regular-file.txt b/regular-file.txt" (keeps a/ prefix)
+cat << EOF | cmp - result1 || exit 1
+a/binary-file
+new-name
+a/mode-only.sh
+a/regular-file.txt
+EOF
+
+# Test --git-prefixes=keep (explicit)
+${LSDIFF} --git-prefixes=keep mixed-git.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
+a/binary-file
+new-name
+a/mode-only.sh
+a/regular-file.txt
+EOF
+
+# Test --git-prefixes=strip
+${LSDIFF} --git-prefixes=strip mixed-git.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+# Expected output explanation:
+# - binary-file: from "diff --git a/binary-file b/binary-file" (strips a/ prefix)
+# - new-name: from "rename to new-name" header (no prefix to strip - same as keep mode)
+# - mode-only.sh: from "diff --git a/mode-only.sh b/mode-only.sh" (strips a/ prefix)
+# - regular-file.txt: from "diff --git a/regular-file.txt b/regular-file.txt" (strips a/ prefix)
+cat << EOF | cmp - result3 || exit 1
+binary-file
+new-name
+mode-only.sh
+regular-file.txt
+EOF
+
+# Test with filterdiff --git-prefixes=strip
+${FILTERDIFF} --git-prefixes=strip -i "*.txt" mixed-git.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+cat << EOF | cmp - result4 || exit 1
+diff --git a/regular-file.txt b/regular-file.txt
+index abcdefg..1234567 100644
+--- a/regular-file.txt
++++ b/regular-file.txt
+@@ -1,2 +1,2 @@
+-old content
++new content
+EOF
+
+# Test with grepdiff --git-prefixes=strip
+${GREPDIFF} --git-prefixes=strip --output-matching=file "content" mixed-git.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+cat << EOF | cmp - result5 || exit 1
+diff --git a/regular-file.txt b/regular-file.txt
+index abcdefg..1234567 100644
+--- a/regular-file.txt
++++ b/regular-file.txt
+@@ -1,2 +1,2 @@
+-old content
++new content
+EOF
+
+# Test invalid argument
+${LSDIFF} --git-prefixes=invalid mixed-git.patch 2>errors6 >result6
+[ $? -eq 1 ] || exit 1
+grep -q "invalid argument to --git-prefixes" errors6 || exit 1
+
+exit 0
diff --git a/tests/git-prefixes-with-strip/run-test b/tests/git-prefixes-with-strip/run-test
new file mode 100755
index 00000000..0e7d4e77
--- /dev/null
+++ b/tests/git-prefixes-with-strip/run-test
@@ -0,0 +1,90 @@
+#!/bin/sh
+
+# This is a filterdiff(1) testcase.
+# Test: Verify interaction between --git-prefixes, -p, and --strip options
+# This tests the order of operations and how they affect each other
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Create a Git patch with a/ and b/ prefixes and nested paths
+cat << EOF > git-test.patch
+diff --git a/level1/level2/file1.txt b/level1/level2/file1.txt
+index abcdef1..1234567 100644
+--- a/level1/level2/file1.txt
++++ b/level1/level2/file1.txt
+@@ -1 +1 @@
+-old content
++new content
+diff --git a/other/path/file2.c b/other/path/file2.c
+index abcdef2..7890abc 100644
+--- a/other/path/file2.c
++++ b/other/path/file2.c
+@@ -1 +1 @@
+-old code
++new code
+EOF
+
+# Test 1: --git-prefixes=keep (default) with -p and --strip
+# With keep: a/level1/level2/file1.txt -> -p1 -> level1/level2/file1.txt for matching
+# Then --strip=2 affects output: removes first 2 components from diff headers
+${FILTERDIFF} --git-prefixes=keep -p1 --strip=2 -i "level1/level2/file1.txt" git-test.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+cat << EOF | cmp - result1 || exit 1
+diff --git level2/file1.txt level2/file1.txt
+index abcdef1..1234567 100644
+--- level2/file1.txt
++++ level2/file1.txt
+@@ -1 +1 @@
+-old content
++new content
+EOF
+
+# Test 2: --git-prefixes=strip with -p and --strip
+# With strip: a/level1/level2/file1.txt -> strip a/ -> level1/level2/file1.txt -> -p1 -> level2/file1.txt for matching
+# Then --strip=2 affects output
+${FILTERDIFF} --git-prefixes=strip -p1 --strip=2 -i "level2/file1.txt" git-test.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << EOF | cmp - result2 || exit 1
+diff --git level2/file1.txt level2/file1.txt
+index abcdef1..1234567 100644
+--- level2/file1.txt
++++ level2/file1.txt
+@@ -1 +1 @@
+-old content
++new content
+EOF
+
+# Test 3: --git-prefixes=strip with -p0 and --strip=1
+# With strip: a/level1/level2/file1.txt -> strip a/ -> level1/level2/file1.txt -> -p0 -> level1/level2/file1.txt for matching
+# Then --strip=1 affects output
+${FILTERDIFF} --git-prefixes=strip -p0 --strip=1 -i "level1/level2/file1.txt" git-test.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+cat << EOF | cmp - result3 || exit 1
+diff --git level1/level2/file1.txt level1/level2/file1.txt
+index abcdef1..1234567 100644
+--- level1/level2/file1.txt
++++ level1/level2/file1.txt
+@@ -1 +1 @@
+-old content
++new content
+EOF
+
+# Test 4: Verify exclusion works correctly with --git-prefixes=strip
+# Exclude "path/file2.c" after stripping a/ and applying -p1
+${FILTERDIFF} --git-prefixes=strip -p1 --strip=1 -x "path/file2.c" git-test.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+cat << EOF | cmp - result4 || exit 1
+diff --git level1/level2/file1.txt level1/level2/file1.txt
+index abcdef1..1234567 100644
+--- level1/level2/file1.txt
++++ level1/level2/file1.txt
+@@ -1 +1 @@
+-old content
++new content
+EOF
+
+exit 0
diff --git a/tests/git-pure-rename/run-test b/tests/git-pure-rename/run-test
new file mode 100755
index 00000000..4cc5808b
--- /dev/null
+++ b/tests/git-pure-rename/run-test
@@ -0,0 +1,137 @@
+#!/bin/sh
+
+# Test for GIT_DIFF_RENAME functionality (similarity index 100%)
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: Pure rename (similarity index 100%)
+cat << 'EOF' > git-pure-rename.patch
+diff --git a/old-name.txt b/new-name.txt
+similarity index 100%
+rename from old-name.txt
+rename to new-name.txt
+EOF
+
+# Test 2: Rename with similarity < 100% (should be treated as copy)
+cat << 'EOF' > git-rename-with-changes.patch
+diff --git a/original.c b/modified.c
+similarity index 85%
+rename from original.c
+rename to modified.c
+index abc123..def456 100644
+--- a/original.c
++++ b/modified.c
+@@ -1,5 +1,6 @@
+ #include
+
+ int main() {
++ printf("Modified!\n");
+ return 0;
+ }
+EOF
+
+# Test 3: Multiple pure renames
+cat << 'EOF' > git-multiple-renames.patch
+diff --git a/file1.txt b/renamed1.txt
+similarity index 100%
+rename from file1.txt
+rename to renamed1.txt
+diff --git a/file2.txt b/renamed2.txt
+similarity index 100%
+rename from file2.txt
+rename to renamed2.txt
+EOF
+
+# Test 4: Rename in subdirectory
+cat << 'EOF' > git-rename-subdir.patch
+diff --git a/src/old.c b/src/new.c
+similarity index 100%
+rename from src/old.c
+rename to src/new.c
+EOF
+
+echo "=== Test 1: lsdiff with pure rename ==="
+${LSDIFF} -s git-pure-rename.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+# For pure renames, both files exist so status should be '!'
+cat << EOF | cmp - result1 || exit 1
+! old-name.txt
+EOF
+
+echo "=== Test 2: filterdiff include pure rename ==="
+${FILTERDIFF} -i "old*" git-pure-rename.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+cat << 'EOF' | cmp - result2 || exit 1
+diff --git a/old-name.txt b/new-name.txt
+similarity index 100%
+rename from old-name.txt
+rename to new-name.txt
+EOF
+
+echo "=== Test 3: filterdiff include by new name (should not match) ==="
+${FILTERDIFF} -i "new*" git-pure-rename.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+# Should be empty because best_name() chooses the old name for renames
+[ -s result3 ] && exit 1
+
+echo "=== Test 4: filterdiff exclude pure rename ==="
+${FILTERDIFF} -x "*name*" git-pure-rename.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+# Should be empty
+[ -s result4 ] && exit 1
+
+echo "=== Test 5: lsdiff with rename that has changes ==="
+${LSDIFF} -s git-rename-with-changes.patch 2>errors5 >result5 || exit 1
+[ -s errors5 ] && exit 1
+
+cat << EOF | cmp - result5 || exit 1
+! a/original.c
+EOF
+
+echo "=== Test 6: lsdiff with multiple renames ==="
+${LSDIFF} -s git-multiple-renames.patch 2>errors6 >result6 || exit 1
+[ -s errors6 ] && exit 1
+
+cat << EOF | cmp - result6 || exit 1
+! file1.txt
+! file2.txt
+EOF
+
+echo "=== Test 7: filterdiff with pattern matching one of multiple renames ==="
+${FILTERDIFF} -i "file1*" git-multiple-renames.patch 2>errors7 >result7 || exit 1
+[ -s errors7 ] && exit 1
+
+cat << 'EOF' | cmp - result7 || exit 1
+diff --git a/file1.txt b/renamed1.txt
+similarity index 100%
+rename from file1.txt
+rename to renamed1.txt
+EOF
+
+echo "=== Test 8: filterdiff with subdirectory rename ==="
+${FILTERDIFF} -i "src/*" git-rename-subdir.patch 2>errors8 >result8 || exit 1
+[ -s errors8 ] && exit 1
+
+cat << 'EOF' | cmp - result8 || exit 1
+diff --git a/src/old.c b/src/new.c
+similarity index 100%
+rename from src/old.c
+rename to src/new.c
+EOF
+
+echo "=== Test 9: Test git-prefixes with renames ==="
+${LSDIFF} --git-prefixes=strip -s git-pure-rename.patch 2>errors9 >result9 || exit 1
+[ -s errors9 ] && exit 1
+
+cat << EOF | cmp - result9 || exit 1
+! old-name.txt
+EOF
+
+# Skip grepdiff test - pure renames have no content to grep
+
+echo "All pure rename tests passed!"
+exit 0
diff --git a/tests/git-rename-issue22/run-test b/tests/git-rename-issue22/run-test
new file mode 100755
index 00000000..4ca7b1e4
--- /dev/null
+++ b/tests/git-rename-issue22/run-test
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+# Test for GitHub Issue #22: File moves are ignored by filterdiff
+# This test case reproduces the exact scenario from the issue
+
+. ${top_srcdir-.}/tests/common.sh
+
+cat << EOF > git-rename.patch
+diff --git a/tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order.html b/tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order-new.html
+similarity index 100%
+rename from tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order.html
+rename to tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order-new.html
+EOF
+
+# Test that filterdiff includes the rename when it matches the filter
+${FILTERDIFF} --git-prefixes=strip -i "*wpt*" git-rename.patch 2>errors >result || exit 1
+[ -s errors ] && exit 1
+
+# The result should contain the complete git diff block
+cat << EOF | cmp - result || exit 1
+diff --git a/tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order.html b/tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order-new.html
+similarity index 100%
+rename from tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order.html
+rename to tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order-new.html
+EOF
+
+# Test that filterdiff excludes the rename when it doesn't match the filter
+${FILTERDIFF} --git-prefixes=strip -i something-else git-rename.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+# The result should be empty
+[ -s result2 ] && exit 1
+
+# Test lsdiff works with renames
+${LSDIFF} --git-prefixes=strip git-rename.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+cat << EOF | cmp - result3 || exit 1
+tests/wpt/web-platform-tests/2dcontext/transformations/2d.transformation.order.html
+EOF
+
+exit 0
diff --git a/tests/grepdiff-original-line-numbers/run-test b/tests/grepdiff-original-line-numbers/run-test
index 4303a8d0..e6e9461e 100755
--- a/tests/grepdiff-original-line-numbers/run-test
+++ b/tests/grepdiff-original-line-numbers/run-test
@@ -24,7 +24,7 @@ index 8a19eed..ae745cf 100644
EOF
# Test --output-matching=hunk (shows adjusted line numbers in hunk headers)
-${GREPDIFF} foo --output-matching=hunk diff 2>errors >hunk_output || exit 1
+${GREPDIFF} --git-prefixes=strip foo --output-matching=hunk diff 2>errors >hunk_output || exit 1
[ -s errors ] && exit 1
# The hunk output shows adjusted line numbers (this is the existing behavior)
@@ -39,7 +39,7 @@ index 8a19eed..ae745cf 100644
EOF
# Test --as-numbered-lines=original-after (the new option for this use case)
-${GREPDIFF} foo --output-matching=hunk --only-match=additions --as-numbered-lines=original-after diff 2>errors2 >numbered_output || exit 1
+${GREPDIFF} --git-prefixes=strip foo --output-matching=hunk --only-match=additions --as-numbered-lines=original-after diff 2>errors2 >numbered_output || exit 1
[ -s errors2 ] && exit 1
# The expected output should show line 12 (original line number from diff)
@@ -51,7 +51,7 @@ index 8a19eed..ae745cf 100644
EOF
# Test --as-numbered-lines=original-before
-${GREPDIFF} foo --output-matching=hunk --only-match=additions --as-numbered-lines=original-before diff 2>errors3 >numbered_before_output || exit 1
+${GREPDIFF} --git-prefixes=strip foo --output-matching=hunk --only-match=additions --as-numbered-lines=original-before diff 2>errors3 >numbered_before_output || exit 1
[ -s errors3 ] && exit 1
# This should show line 8 for the "before" version (original line number from diff)
@@ -63,7 +63,7 @@ index 8a19eed..ae745cf 100644
EOF
# Test that regular --as-numbered-lines=after still works with adjusted line numbers
-${GREPDIFF} foo --output-matching=hunk --only-match=additions --as-numbered-lines=after diff 2>errors4 >numbered_adjusted_output || exit 1
+${GREPDIFF} --git-prefixes=strip foo --output-matching=hunk --only-match=additions --as-numbered-lines=after diff 2>errors4 >numbered_adjusted_output || exit 1
[ -s errors4 ] && exit 1
# This should show line 8 (adjusted line number, old behavior)
diff --git a/tests/malformed-diff-headers/run-test b/tests/malformed-diff-headers/run-test
new file mode 100755
index 00000000..9a154605
--- /dev/null
+++ b/tests/malformed-diff-headers/run-test
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+# Test malformed diff headers and error handling paths
+# This test covers the flush_continue code path in filterdiff
+
+. ${top_srcdir-.}/tests/common.sh
+
+# Test 1: Malformed diff header (triggers flush_continue path with verbose output)
+cat << EOF > malformed-diff.patch
+diff --git a/test.txt b/test.txt
+some invalid header line here
+EOF
+
+# This should trigger the flush_continue path and output the malformed headers in verbose mode
+${FILTERDIFF} --verbose malformed-diff.patch 2>errors1 >result1 || exit 1
+[ -s errors1 ] && exit 1
+
+# Should output the original headers due to verbose mode
+grep -q "diff --git a/test.txt b/test.txt" result1 || exit 1
+grep -q "some invalid header line here" result1 || exit 1
+
+# Test 2: Same malformed diff but with exclude patterns (different code path)
+${FILTERDIFF} --verbose -x "*.nonexistent" malformed-diff.patch 2>errors2 >result2 || exit 1
+[ -s errors2 ] && exit 1
+
+# Should also output the headers due to exclude pattern + verbose
+grep -q "diff --git a/test.txt b/test.txt" result2 || exit 1
+grep -q "some invalid header line here" result2 || exit 1
+
+# Test 3: Malformed diff with clean option (should NOT output headers)
+# Note: --verbose and --clean are mutually exclusive, so we test clean separately
+${FILTERDIFF} --clean malformed-diff.patch 2>errors3 >result3 || exit 1
+[ -s errors3 ] && exit 1
+
+# Should NOT contain the malformed header due to --clean
+[ -s result3 ] && exit 1
+
+# Test 4: Multiple malformed headers
+cat << EOF > multi-malformed.patch
+diff --git a/file1.txt b/file1.txt
+invalid line 1
+invalid line 2
+diff --git a/file2.txt b/file2.txt
+another invalid line
+EOF
+
+${FILTERDIFF} --verbose multi-malformed.patch 2>errors4 >result4 || exit 1
+[ -s errors4 ] && exit 1
+
+# Should contain all the malformed content
+grep -q "invalid line 1" result4 || exit 1
+grep -q "invalid line 2" result4 || exit 1
+grep -q "another invalid line" result4 || exit 1
+
+exit 0