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