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