diff --git a/Makefile.am b/Makefile.am index a973eeb4..d4fa70d7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -116,6 +116,13 @@ TESTS = tests/newline1/run-test \ tests/combine2/run-test \ tests/combine3/run-test \ tests/combine4/run-test \ + tests/combine5/run-test \ + tests/combine6/run-test \ + tests/combine-devnull/run-test \ + tests/combine-devnull-p/run-test \ + tests/combine-no-match/run-test \ + tests/interdiff-devnull/run-test \ + tests/flipdiff-devnull/run-test \ tests/gendiff1/run-test \ tests/gendiff2/run-test \ tests/comma/run-test \ diff --git a/doc/patchutils.xml b/doc/patchutils.xml index c56860dc..9784160b 100644 --- a/doc/patchutils.xml +++ b/doc/patchutils.xml @@ -162,7 +162,11 @@ option to GNU patch 1 - .) + .) If this option is not specified, + interdiff will automatically + determine the optimal number of path components to + strip by finding the smallest value where at least + one file matches between the two patches. @@ -464,7 +468,11 @@ option to GNU patch 1 - .) + .) If this option is not specified, + combinediff will automatically + determine the optimal number of path components to + strip by finding the smallest value where at least + one file matches between the two patches. @@ -2922,7 +2930,11 @@ will pipe patch of file #2 to vim - -R option to GNU patch 1 - .) + .) If this option is not specified, + flipdiff will automatically + determine the optimal number of path components to + strip by finding the smallest value where at least + one file matches between the two patches. diff --git a/src/interdiff.c b/src/interdiff.c index d604fe1f..f0d4a7c0 100644 --- a/src/interdiff.c +++ b/src/interdiff.c @@ -95,6 +95,7 @@ static int num_diff_opts = 0; static unsigned int max_context_real = 3, max_context = 3; static int context_specified = 0; static int ignore_components = 0; +static int ignore_components_specified = 0; static int unzip = 0; static int no_revert_omitted = 0; static int debug = 0; @@ -103,6 +104,7 @@ static struct patlist *pat_drop_context = NULL; static struct file_list *files_done = NULL; static struct file_list *files_in_patch2 = NULL; +static struct file_list *files_in_patch1 = NULL; /* checks whether file needs processing and sets context */ static int @@ -147,6 +149,49 @@ file_in_list (struct file_list *list, const char *file) return -1; } +/* Determine the best ignore_components value when none was specified. + * Try different values from 0 up to a reasonable maximum, and find the + * smallest value where at least one file from patch1 matches a file from patch2. + */ +static int +determine_ignore_components (struct file_list *list1, struct file_list *list2) +{ + int max_components = 0; + int p; + + /* Find the maximum number of path components in any file */ + for (struct file_list *l = list1; l; l = l->next) { + if (strcmp(l->file, "/dev/null") != 0) { + int components = num_pathname_components(l->file); + if (components > max_components) + max_components = components; + } + } + for (struct file_list *l = list2; l; l = l->next) { + if (strcmp(l->file, "/dev/null") != 0) { + int components = num_pathname_components(l->file); + if (components > max_components) + max_components = components; + } + } + + /* Try different -p values, starting from 0 */ + for (p = 0; p <= max_components; p++) { + for (struct file_list *l1 = list1; l1; l1 = l1->next) { + const char *stripped1 = stripped(l1->file, p); + for (struct file_list *l2 = list2; l2; l2 = l2->next) { + const char *stripped2 = stripped(l2->file, p); + if (!strcmp(stripped1, stripped2)) { + return p; + } + } + } + } + + /* If no match found, return 0 as default */ + return 0; +} + static void free_list (struct file_list *list) { @@ -178,7 +223,7 @@ add_line (struct lines_info *lines, const char *line, size_t length, { struct lines *make; struct lines *at; - + make = xmalloc (sizeof *make); make->n = n; make->line = xmalloc (length + 1); @@ -243,7 +288,7 @@ merge_lines (struct lines_info *lines1, struct lines_info *lines2) struct lines *at, *at_prev, *make; - /* + /* * Note: we don't care about the tail member here - not needed * anymore. */ @@ -619,7 +664,7 @@ do_output_patch1_only (FILE *p1, FILE *out, int not_reverted) orig_lines = orig_num_lines (d1); new_lines = new_num_lines (d2); } - + free (d1); free (d2); @@ -1138,7 +1183,7 @@ output_delta (FILE *p1, FILE *p2, FILE *out) memcpy (argv + 2, diff_opts, num_diff_opts * sizeof (char *)); memcpy (argv + 2 + num_diff_opts, (char *[]) { tmpp1, tmpp2, NULL }, (2 + 1) * sizeof (char *)); in = xpipe (DIFF, &child, "r", argv); - + /* Eat the first line */ for (;;) { int ch = fgetc (in); @@ -1270,22 +1315,23 @@ copy_residue (FILE *p2, FILE *out) return 0; } +/* Generic patch indexing function that can handle both patch1 and patch2 */ static int -index_patch2 (FILE *p2) +index_patch_generic (FILE *patch_file, struct file_list **file_list, int need_skip_content) { char *line = NULL; size_t linelen = 0; int is_context = 0; int file_is_empty = 1; - /* Index patch2 */ - while (!feof (p2)) { + /* Index patch */ + while (!feof (patch_file)) { unsigned long skip; char *names[2]; char *p, *end; - long pos = ftell (p2); + long pos = ftell (patch_file); - if (getline (&line, &linelen, p2) == -1) + if (getline (&line, &linelen, patch_file) == -1) break; file_is_empty = 0; @@ -1301,7 +1347,7 @@ index_patch2 (FILE *p2) names[0] = filename_from_header (line + 4); - if (getline (&line, &linelen, p2) == -1) { + if (getline (&line, &linelen, patch_file) == -1) { free (names[0]); break; } @@ -1313,38 +1359,44 @@ index_patch2 (FILE *p2) names[1] = filename_from_header (line + 4); - if (getline (&line, &linelen, p2) == -1) { - free (names[0]); - free (names[1]); - break; - } - - if (strncmp (line, "@@ ", 3)) - goto try_next; + /* For patch2, we need to handle the @@ line and skip content */ + if (need_skip_content) { + if (getline (&line, &linelen, patch_file) == -1) { + free (names[0]); + free (names[1]); + break; + } - p = strchr (line + 3, '+'); - if (!p) - goto try_next; - p = strchr (p, ','); - if (p) { - /* Like '@@ -1,3 +1,3 @@' */ - p++; - skip = strtoul (p, &end, 10); - if (p == end) + if (strncmp (line, "@@ ", 3)) goto try_next; - } else - /* Like '@@ -1 +1 @@' */ - skip = 1; - - add_to_list (&files_in_patch2, best_name (2, names), pos); - - while (skip--) { - if (getline (&line, &linelen, p2) == -1) - break; - if (line[0] == '-') - /* Doesn't count towards skip count */ - skip++; + p = strchr (line + 3, '+'); + if (!p) + goto try_next; + p = strchr (p, ','); + if (p) { + /* Like '@@ -1,3 +1,3 @@' */ + p++; + skip = strtoul (p, &end, 10); + if (p == end) + goto try_next; + } else + /* Like '@@ -1 +1 @@' */ + skip = 1; + + add_to_list (file_list, best_name (2, names), pos); + + while (skip--) { + if (getline (&line, &linelen, patch_file) == -1) + break; + + if (line[0] == '-') + /* Doesn't count towards skip count */ + skip++; + } + } else { + /* For patch1, just add to list */ + add_to_list (file_list, best_name (2, names), pos); } try_next: @@ -1355,12 +1407,18 @@ index_patch2 (FILE *p2) if (line) free (line); - if (file_is_empty || files_in_patch2) + if (file_is_empty || *file_list) return 0; else return 1; } +static int +index_patch2 (FILE *p2) +{ + return index_patch_generic (p2, &files_in_patch2, 1); +} + /* With flipdiff we have two patches we want to reorder. The * algorithm is: * @@ -1424,7 +1482,7 @@ patch2_removes_line (unsigned long line, /* line number after patch1 */ return 1; } } - + return 0; } @@ -1908,6 +1966,12 @@ no_patch (const char *f) return 0; } +static int +index_patch1 (FILE *p1) +{ + return index_patch_generic (p1, &files_in_patch1, 0); +} + static int interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) { @@ -1926,6 +1990,20 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) if (index_patch2 (p2)) no_patch (patch2); + /* Index patch1 and determine ignore_components if not specified */ + if (index_patch1 (p1)) + no_patch (patch1); + + /* Determine ignore_components automatically if not explicitly specified */ + if (!ignore_components_specified) { + ignore_components = determine_ignore_components (files_in_patch1, files_in_patch2); + if (debug) + fprintf (stderr, "Auto-determined -p%d\n", ignore_components); + } + + /* Reset file pointer for patch1 */ + rewind (p1); + /* Search for next file to patch */ while (!feof (p1)) { char *names[2]; @@ -1966,7 +2044,7 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) free (names[0]); free (names[1]); patch_found = 1; - + /* check if we need to process it and init context */ if (!check_filename(p)) { add_to_list (&files_done, p, 0); @@ -2020,6 +2098,7 @@ interdiff (FILE *p1, FILE *p2, const char *patch1, const char *patch2) fclose (flip1); if (flip2) fclose (flip2); + free_list (files_in_patch1); free_list (files_in_patch2); free_list (files_done); if (line) @@ -2163,6 +2242,7 @@ main (int argc, char *argv[]) ignore_components = strtoul (optarg, &end, 0); if (optarg == end) syntax (1); + ignore_components_specified = 1; break; case 'q': human_readable = 0; @@ -2221,7 +2301,7 @@ main (int argc, char *argv[]) if (unzip && flipdiff_inplace) error (EXIT_FAILURE, 0, "-z and --in-place are mutually exclusive."); - + /* Add default color=always if no color option was specified and we're in a terminal */ int has_color_option = 0; for (int i = 0; i < num_diff_opts; i++) { @@ -2237,7 +2317,7 @@ main (int argc, char *argv[]) if (optind + 2 != argc) syntax (1); - + if (unzip) { p1 = xopen_unzip (argv[optind], "rb"); p2 = xopen_unzip (argv[optind + 1], "rb"); diff --git a/tests/combine-devnull-p/run-test b/tests/combine-devnull-p/run-test new file mode 100755 index 00000000..837c6860 --- /dev/null +++ b/tests/combine-devnull-p/run-test @@ -0,0 +1,53 @@ +#!/bin/sh + +# This is a combinediff(1) testcase for the /dev/null issue with explicit -p parameter. +# When -p is explicitly specified, it should use the traditional logic. + +. ${top_srcdir-.}/tests/common.sh + +# Create patches with same structure but different paths +cat << EOF > patch1 +--- /dev/null ++++ path1/file.txt +@@ -0,0 +1,3 @@ ++a ++b ++c +EOF + +cat << EOF > patch2 +--- path2/file.txt ++++ path1/file.txt +@@ -1,3 +1,2 @@ + a + b +-c +EOF + +# With -p0, these should NOT match (path1/file.txt vs path2/file.txt) +${COMBINEDIFF} -p0 patch1 patch2 2>errors >actual || exit 1 +[ -s errors ] && exit 1 + +# Should show separate patches +grep -q "only in patch2:" actual || exit 1 + +# With -p1, these SHOULD match (file.txt vs file.txt) +${COMBINEDIFF} -p1 patch1 patch2 2>errors >actual || exit 1 +[ -s errors ] && exit 1 + +# Should show combined patch +grep -q "only in patch2:" actual && exit 1 + +# Verify the combined result is correct +cat << EOF > expected +diff -u /dev/null path1/file.txt +--- /dev/null ++++ path1/file.txt +@@ -0,0 +1,2 @@ ++a ++b +EOF + +diff -u expected actual || exit 1 + +exit 0 diff --git a/tests/combine-devnull/run-test b/tests/combine-devnull/run-test new file mode 100755 index 00000000..fa3a8945 --- /dev/null +++ b/tests/combine-devnull/run-test @@ -0,0 +1,54 @@ +#!/bin/sh + +# This is a combinediff(1) testcase for the /dev/null issue. +# Issue: https://github.com/twaugh/patchutils/issues/23 + +. ${top_srcdir-.}/tests/common.sh + +# Create the first patch that creates a new file +cat << EOF > patch1 +--- /dev/null ++++ b/txt +@@ -0,0 +1,3 @@ ++a ++b ++c +EOF + +# Create the second patch that modifies the same file +cat << EOF > patch2 +--- a/txt ++++ b/txt +@@ -1,3 +1,2 @@ + a + b +-c +EOF + +# Expected combined output should be: +cat << EOF > expected +diff -u /dev/null b/txt +--- /dev/null ++++ b/txt +@@ -0,0 +1,2 @@ ++a ++b +EOF + +# Run combinediff +${COMBINEDIFF} patch1 patch2 2>errors >actual || exit 1 +[ -s errors ] && exit 1 + +# Compare the actual output with expected +diff -u expected actual || exit 1 + +# Test that the combined patch actually works +rm -f txt +${PATCH} -p1 < actual || exit 1 +cat << EOF > expected_result +a +b +EOF +diff -u expected_result txt || exit 1 + +exit 0 diff --git a/tests/combine-no-match/run-test b/tests/combine-no-match/run-test new file mode 100755 index 00000000..9f0a8284 --- /dev/null +++ b/tests/combine-no-match/run-test @@ -0,0 +1,37 @@ +#!/bin/sh + +# Test combinediff when no files match at any -p level +# Should fall back to showing separate patches + +. ${top_srcdir-.}/tests/common.sh + +# Create first patch with one file +cat << EOF > patch1 +--- /dev/null ++++ b/file1.txt +@@ -0,0 +1,2 @@ ++content1 ++line2 +EOF + +# Create second patch with completely different file +cat << EOF > patch2 +--- a/completely/different/file2.txt ++++ b/completely/different/file2.txt +@@ -1,1 +1,2 @@ + existing ++new line +EOF + +# Run combinediff - should show separate patches since no files match +${COMBINEDIFF} patch1 patch2 2>errors >actual || exit 1 +[ -s errors ] && exit 1 + +# Should show "only in patch2:" indicating patches weren't combined +grep -q "only in patch2:" actual || exit 1 + +# Should contain both patches +grep -q "file1.txt" actual || exit 1 +grep -q "file2.txt" actual || exit 1 + +exit 0 diff --git a/tests/combine5/run-test b/tests/combine5/run-test new file mode 100755 index 00000000..35cbedcc --- /dev/null +++ b/tests/combine5/run-test @@ -0,0 +1,45 @@ +#!/bin/sh + +# Test combinediff with multiple path levels requiring -p2 +# This tests the automatic detection of deeper path stripping + +. ${top_srcdir-.}/tests/common.sh + +# Create first patch with deeply nested path +cat << EOF > patch1 +--- /dev/null ++++ b/src/deep/nested/file.c +@@ -0,0 +1,2 @@ ++#include ++int main() { return 0; } +EOF + +# Create second patch modifying the same file with different path prefix +cat << EOF > patch2 +--- a/src/deep/nested/file.c ++++ b/src/deep/nested/file.c +@@ -1,2 +1,3 @@ + #include ++#include + int main() { return 0; } +EOF + +# Expected combined output (should auto-detect -p1) +cat << EOF > expected +diff -u /dev/null b/src/deep/nested/file.c +--- /dev/null ++++ b/src/deep/nested/file.c +@@ -0,0 +1,3 @@ ++#include ++#include ++int main() { return 0; } +EOF + +# Run combinediff +${COMBINEDIFF} patch1 patch2 2>errors >actual || exit 1 +[ -s errors ] && exit 1 + +# Compare the actual output with expected +diff -u expected actual || exit 1 + +exit 0 diff --git a/tests/combine6/run-test b/tests/combine6/run-test new file mode 100755 index 00000000..52cb2a84 --- /dev/null +++ b/tests/combine6/run-test @@ -0,0 +1,35 @@ +#!/bin/sh + +# Test combinediff with file deletion (regular file to /dev/null) +# This tests /dev/null handling in the reverse direction + +. ${top_srcdir-.}/tests/common.sh + +# Create first patch that creates a file +cat << EOF > patch1 +--- /dev/null ++++ b/tempfile.txt +@@ -0,0 +1,3 @@ ++temporary ++content ++here +EOF + +# Create second patch that deletes the same file +cat << EOF > patch2 +--- a/tempfile.txt ++++ /dev/null +@@ -1,3 +0,0 @@ +-temporary +-content +-here +EOF + +# Run combinediff +${COMBINEDIFF} patch1 patch2 2>errors >actual || exit 1 +[ -s errors ] && exit 1 + +# Expected combined output should be empty (create then delete = no change) +diff -u /dev/null actual || exit 1 + +exit 0 diff --git a/tests/flipdiff-devnull/run-test b/tests/flipdiff-devnull/run-test new file mode 100755 index 00000000..8f268da7 --- /dev/null +++ b/tests/flipdiff-devnull/run-test @@ -0,0 +1,51 @@ +#!/bin/sh + +# Test flipdiff with /dev/null automatic -p detection +# This tests that flipdiff can auto-detect -p values when dealing with /dev/null + +. ${top_srcdir-.}/tests/common.sh + +# Create first patch (creates a file) +cat << EOF > patch1 +--- /dev/null ++++ b/testfile.c +@@ -0,0 +1,2 @@ ++#include ++int main() { return 0; } +EOF + +# Create second patch (modifies the same file) +cat << EOF > patch2 +--- a/testfile.c ++++ b/testfile.c +@@ -1,2 +1,3 @@ + #include ++#include + int main() { return 0; } +EOF + +# Expected flipdiff output (should auto-detect -p1) +cat << EOF > expected +--- a/testfile.c ++++ b/testfile.c +@@ -0,0 +1 @@ ++#include + +=== 8< === cut here === 8< === + +--- /dev/null ++++ b/testfile.c +@@ -1 +1,3 @@ ++#include + #include ++int main() { return 0; } +EOF + +# Run flipdiff - should auto-detect -p1 and produce flipped patches +${FLIPDIFF} patch1 patch2 2>errors >actual || exit 1 +[ -s errors ] && exit 1 + +# Compare the actual output with expected +diff -u expected actual || exit 1 + +exit 0 diff --git a/tests/interdiff-devnull/run-test b/tests/interdiff-devnull/run-test new file mode 100755 index 00000000..58d5b969 --- /dev/null +++ b/tests/interdiff-devnull/run-test @@ -0,0 +1,49 @@ +#!/bin/sh + +# Test interdiff with /dev/null automatic -p detection +# This tests that interdiff can auto-detect -p values when dealing with /dev/null + +. ${top_srcdir-.}/tests/common.sh + +# Create first patch (creates a file with initial content) +cat << EOF > patch1 +--- /dev/null ++++ b/newfile.txt +@@ -0,0 +1,3 @@ ++line one ++line two ++line three +EOF + +# Create second patch (creates the same file with different content) +cat << EOF > patch2 +--- /dev/null ++++ a/newfile.txt +@@ -0,0 +1,4 @@ ++line one ++inserted line ++line two ++line three +EOF + +# Expected interdiff output (should auto-detect -p1) +# This shows what needs to change to go from patch1's result to patch2's result +cat << EOF > expected +diff -u b/newfile.txt a/newfile.txt +--- b/newfile.txt ++++ a/newfile.txt +@@ -1,3 +1,4 @@ + line one ++inserted line + line two + line three +EOF + +# Run interdiff +${INTERDIFF} patch1 patch2 2>errors >actual || exit 1 +[ -s errors ] && exit 1 + +# Compare the actual output with expected +diff -u expected actual || exit 1 + +exit 0