[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

[PATCH 6/7] Implement 'm' option for conffile merging during conflict resolution



From: Sean Finney <seanius@debian.org>
Date: Mon, 14 Dec 2009 08:27:36 +0100

The conflict resolution prompt has been updated with a new option for
automatic merging:

          M     : attempt to automatically merge the differences

This functions by performing a 3-way diff using the new conffile, the
currently-installed conffile, and the pristine version of the conffile
from the currently installed version of the package (which is retreived
from the conffile database). The merge output is generated alongside the
conffile, in the form <path>.dpkg-merge.

On a successful merge, the merge output replaces the conffile, and dpkg
continues as if the admin had selected to keep the old conffile. On merge
failure the admin is informed of the failure and the the conflict prompt
is redisplayed. In such an event the merge output will still be present
in case it is useful for merge resolution.

This commit also contains updates to the conffiledb unit tests, covering
the new merge functionality.

[jn: rebase

Conflicts:

	src/configure.c
]

Signed-off-by: Jonathan Nieder <jrnieder@gmail.com>
---
Is "diff3 -m" the state of the art for this?  Well, it will do to
start.  In the long term maybe some people will want to use some other
tool to perform the merge, like RCS "merge" or something Config::Model
based.

In the general case, the right merge result may depend on the content
of other files (for example, if the user renamed a file so the new
pristine version should be renamed, too).  Luckily I don't think the
layout is tying our hands, so that could happen in the future.

Tiny nitpick: I think I'd rather this not go on to 'N' automatically
but return to the "what now?" prompt so the user can inspect the
automatic merge result and decide whether she is happy with it, though
I could be easily convinced otherwise.

 lib/dpkg/dpkg.h              |    2 +
 lib/dpkg/test/t-conffiledb.c |  124 ++++++++++++++++++++++++++++++++++++++++++
 src/conffiledb.c             |   75 +++++++++++++++++++++++++-
 src/conffiledb.h             |    2 +
 src/configure.c              |   16 +++++-
 5 files changed, 217 insertions(+), 2 deletions(-)

diff --git a/lib/dpkg/dpkg.h b/lib/dpkg/dpkg.h
index a3f5e70..2406ecd 100644
--- a/lib/dpkg/dpkg.h
+++ b/lib/dpkg/dpkg.h
@@ -49,6 +49,7 @@ DPKG_BEGIN_DECLS
 #define DPKGNEWEXT         ".dpkg-new"
 #define DPKGOLDEXT         ".dpkg-old"
 #define DPKGDISTEXT        ".dpkg-dist"
+#define DPKGMERGEEXT       ".dpkg-merge"
 
 #define CONTROLFILE        "control"
 #define CONFFILESFILE      "conffiles"
@@ -92,6 +93,7 @@ DPKG_BEGIN_DECLS
 #define RM		"rm"
 #define FIND		"find"
 #define DIFF		"diff"
+#define DIFF3		"diff3"
 
 #define FIND_EXPRSTARTCHARS "-(),!"
 
diff --git a/lib/dpkg/test/t-conffiledb.c b/lib/dpkg/test/t-conffiledb.c
index 561b159..8b07e5f 100644
--- a/lib/dpkg/test/t-conffiledb.c
+++ b/lib/dpkg/test/t-conffiledb.c
@@ -212,6 +212,128 @@ test_conffiledb_storage(void)
 	free(expected_path);
 }
 
+/**
+ * Test that the autmatic merging functionality works as intended.
+ */
+static void
+test_conffiledb_merge(void)
+{
+	int install_fd, verify_fd, pipefds[2];
+	char *content_old = m_strdup("one\ntwo\nthree\nfour\n");
+	char *content_local = m_strdup("one\nfoo\nthree\nfour\n");
+	char *content_new = m_strdup("one\ntwo\nthree\nfour\nfive\n");
+	char *content_merged = m_strdup("one\nfoo\nthree\nfour\nfive\n");
+	char *read_buf = m_malloc(64); /* just needs to be bigger than above */
+	int fake_sz_old = strlen(content_old);
+	int fake_sz_local = strlen(content_local);
+	int fake_sz_new = strlen(content_new);
+	size_t merge_result, read_sz;
+
+	/* setup a fake pipe like before */
+	if (pipe(pipefds))
+		ohshite("old pipe failed");
+	if (write(pipefds[1], content_old, fake_sz_old) != fake_sz_old)
+		ohshite("old write failed");
+	if (close(pipefds[1]))
+		ohshite("old close failed");
+	conffiledb_register_new_conffile(&pkg_a, pkg_a_cnff_1, pipefds[0],
+	                                 fake_sz_old);
+
+	/* install the "modified" version that the "admin" changed */
+	install_fd = open(pkg_a_cnff_1, O_WRONLY|O_CREAT|O_TRUNC, 0700);
+	if (install_fd < 0)
+		ohshite("install open failed");
+	if (write(install_fd, content_local, fake_sz_local) != fake_sz_local)
+		ohshite("install write failed");
+	if (close(install_fd) != 0)
+		ohshite("install close failed");
+
+	/* here now pretend it's later and we're installing a new file */
+	if (pipe(pipefds))
+		ohshite("new pipe failed");
+	if (write(pipefds[1], content_new, fake_sz_new) != fake_sz_new)
+		ohshite("new write failed");
+	if (close(pipefds[1]))
+		ohshite("new close failed");
+	conffiledb_register_new_conffile(&pkg_a2, pkg_a_cnff_1, pipefds[0],
+	                                 fake_sz_new);
+
+	merge_result = conffiledb_automerge(&pkg_a2, pkg_a_cnff_1);
+	test_pass(merge_result == 0);
+
+	/* now double check everything is as expected */
+	verify_fd = open(pkg_a_cnff_1, O_RDONLY);
+	if(verify_fd < 0)
+		ohshite("verify open failed");
+	read_sz = read(verify_fd, read_buf, 64);
+	test_pass(read_sz == strlen(content_merged));
+	read_buf[read_sz] = '\0';
+	test_str(read_buf, ==, content_merged);
+
+	if (close(verify_fd))
+		ohshite("verify close failed");
+
+	free(content_old);
+	free(content_local);
+	free(content_new);
+	free(content_merged);
+	free(read_buf);
+}
+
+/**
+ * Test that the autmatic merging functionality fails when it should fail.
+ */
+static void
+test_conffiledb_merge_failure(void)
+{
+	int install_fd, pipefds[2];
+	char *content_old = m_strdup("one\ntwo\nthree\nfour\n");
+	char *content_local = m_strdup("one\nfoo\nthree\nfour\n");
+	char *content_new = m_strdup("one\n2\nthree\nfour\n");
+	char *read_buf = m_malloc(64); /* just needs to be bigger than above */
+	int fake_sz_old = strlen(content_old);
+	int fake_sz_local = strlen(content_local);
+	int fake_sz_new = strlen(content_new);
+	size_t merge_result;
+
+	/* setup a fake pipe like before */
+	if (pipe(pipefds))
+		ohshite("old pipe failed");
+	if (write(pipefds[1], content_old, fake_sz_old) != fake_sz_old)
+		ohshite("old write failed");
+	if (close(pipefds[1]))
+		ohshite("old close failed");
+	conffiledb_register_new_conffile(&pkg_a, pkg_a_cnff_1, pipefds[0],
+	                                 fake_sz_old);
+
+	/* install the "modified" version that the "admin" changed */
+	install_fd = open(pkg_a_cnff_1, O_WRONLY|O_CREAT|O_TRUNC, 0700);
+	if (install_fd < 0)
+		ohshite("install open failed");
+	if (write(install_fd, content_local, fake_sz_local) != fake_sz_local)
+		ohshite("install write failed");
+	if (close(install_fd) != 0)
+		ohshite("install close failed");
+
+	/* here now pretend it's later and we're installing a new file */
+	if (pipe(pipefds))
+		ohshite("new pipe failed");
+	if (write(pipefds[1], content_new, fake_sz_new) != fake_sz_new)
+		ohshite("new write failed");
+	if (close(pipefds[1]))
+		ohshite("new close failed");
+	conffiledb_register_new_conffile(&pkg_a2, pkg_a_cnff_1, pipefds[0],
+	                                 fake_sz_new);
+
+	merge_result = conffiledb_automerge(&pkg_a2, pkg_a_cnff_1);
+	test_pass(merge_result != 0);
+
+	free(content_old);
+	free(content_local);
+	free(content_new);
+	free(read_buf);
+}
+
 static void
 test(void)
 {
@@ -219,6 +341,8 @@ test(void)
 
 	test_conffiledb_paths();
 	test_conffiledb_storage();
+	test_conffiledb_merge();
+	test_conffiledb_merge_failure();
 
 	teardown();
 }
diff --git a/src/conffiledb.c b/src/conffiledb.c
index 2dec3b5..baaf7ba 100644
--- a/src/conffiledb.c
+++ b/src/conffiledb.c
@@ -25,17 +25,19 @@
 #include <unistd.h>
 #include <sys/types.h>
 #include <sys/stat.h>
-#include <sys/wait.h>
 #include <fcntl.h>
 #include <stdlib.h>
 #include <errno.h>
+#include <sys/wait.h>
 
 #include <dpkg/dpkg.h>
 #include <dpkg/i18n.h>
 #include <dpkg/buffer.h>
 #include <dpkg/path.h>
+#include <dpkg/file.h>
 #include <dpkg/dpkg-db.h>
 #include <dpkg/varbuf.h>
+#include <dpkg/subproc.h>
 
 #include "main.h"
 #include "conffiledb.h"
@@ -261,3 +263,74 @@ conffiledb_remove(const struct pkginfo *pkg,
 
 	free(p);
 }
+
+/**
+ * Attempt to automagically merge a 3-way delta for a conffile.
+ *
+ * Using the pristine version of the conffile a merge is attempted via
+ * a forked call to diff3(1). The output is stored alongside the conffile
+ * using the convention \<file\>.dpkg-merge. If the merge is successful
+ * the conffile is replaced by the output from the merge, otherwise
+ * the merge output is left for inspection.
+ *
+ * @param pkg The package owning the conffile.
+ * @param path The path to the installed conffile.
+ *
+ * @return The exit status of the underlying call to diff3(1).
+ */
+int
+conffiledb_automerge(const struct pkginfo *pkg, const char *path)
+{
+	int res, merge_fd, merge_pipe[2];
+	pid_t child_pid;
+	struct varbuf merge_output_fname = VARBUF_INIT;
+	char *cf_old, *cf_new;
+
+	cf_old = conffiledb_path(pkg, &pkg->configversion, path);
+	cf_new = conffiledb_path(pkg, &pkg->available.version, path);
+
+	/* create a file for the merge results */
+	varbufprintf(&merge_output_fname, "%s%s", path, DPKGMERGEEXT);
+	merge_fd = open(merge_output_fname.buf, O_CREAT|O_TRUNC|O_WRONLY, 0600);
+	if (merge_fd == -1)
+		ohshite(_("open '%s' failed"), merge_output_fname.buf);
+
+	/* fork a child for diff3 and catch its stdout in a pipe */
+	if (pipe(merge_pipe) == -1)
+		ohshite(_("pipe failed"));
+	child_pid = m_fork();
+	if (!child_pid) {
+		if(dup2(merge_pipe[1], STDOUT_FILENO) == -1)
+			ohshite(_("dup2 failed in child process"));
+		if(close(merge_pipe[0]) == -1 || close(merge_pipe[1]) == -1)
+			ohshite(_("close on pipe failed"));
+		execlp(DIFF3, "diff3", "-m", cf_new, cf_old, path, NULL);
+		ohshite(_("failed to exec '%s' for merge"), DIFF3);
+	}
+	debug(dbg_conffdetail, "conffiledb_automerge: merging");
+
+	/* now copy the piped output into the merge results file */
+	if (close(merge_pipe[1]) == -1)
+		ohshite(_("close on pipe failed"));
+	fd_fd_copy(merge_pipe[0], merge_fd, -1, _("conffiledb merge output"));
+	if (close(merge_fd) == -1)
+		ohshite(_("close '%s' failed"), merge_output_fname.buf);
+	file_copy_perms(path, merge_output_fname.buf);
+	if (close(merge_pipe[0]) == -1)
+		ohshite(_("close on pipe failed"));
+
+	res = subproc_wait(child_pid, DIFF3);
+	if (WEXITSTATUS(res) == 0) {
+		/* rename the merge output to the final destination */
+		if (rename(merge_output_fname.buf, path) == -1)
+			ohshite(_("rename '%s' failed"),
+			        merge_output_fname.buf);
+	}
+
+	varbuffree(&merge_output_fname);
+
+	free(cf_new);
+	free(cf_old);
+
+	return res;
+}
diff --git a/src/conffiledb.h b/src/conffiledb.h
index 4bb50eb..d0db9de 100644
--- a/src/conffiledb.h
+++ b/src/conffiledb.h
@@ -69,4 +69,6 @@ int conffiledb_open_conffile(const struct pkginfo *pkg,
 void conffiledb_remove(const struct pkginfo *pkg,
                        const struct versionrevision *version);
 
+int conffiledb_automerge(const struct pkginfo *pkg, const char *path);
+
 #endif /* DPKG_CONFFILEDB_H */
diff --git a/src/configure.c b/src/configure.c
index 74ef148..7fcdfea 100644
--- a/src/configure.c
+++ b/src/configure.c
@@ -50,6 +50,7 @@
 
 #include "conffiledb.h"
 #include "filesdb.h"
+#include "conffiledb.h"
 #include "main.h"
 
 static int conffoptcells[2][2] = {
@@ -657,6 +658,7 @@ promptconfaction(struct pkginfo *pkg, const char *cfgfile,
 		          "    N or O  : keep your currently-installed version\n"
 		          "      D     : show the differences from your current version\n"
 		          "      P     : show the differences from the previous debian version\n"
+		          "      M     : attempt to automatically merge the differences\n"
 		          "      Z     : start a shell to examine the situation\n"));
 
 		if (what & cfof_keep)
@@ -667,7 +669,7 @@ promptconfaction(struct pkginfo *pkg, const char *cfgfile,
 		s = strrchr(cfgfile, '/');
 		if (!s || !*++s)
 			s = cfgfile;
-		fprintf(stderr, "*** %s (Y/I/N/O/D/P/Z) %s ? ",
+		fprintf(stderr, "*** %s (Y/I/N/O/D/P/M/Z) %s ? ",
 		        s,
 		        (what & cfof_keep) ? _("[default=N]") :
 		        (what & cfof_install) ? _("[default=Y]") :
@@ -709,6 +711,18 @@ promptconfaction(struct pkginfo *pkg, const char *cfgfile,
 			free(prev_conffile);
 		}
 
+		if (cc == 'm') {
+			fprintf(stderr, _("attempting automatic merge of %s: "), cfgfile);
+			if (conffiledb_automerge(pkg, cfgfile) == 0) {
+				fprintf(stderr, _("success.\n"));
+				/* so let's just pretend they said "keep my
+				 * version" ;) */
+				cc = 'n';
+			} else {
+				fprintf(stderr, _("failed.\n"));
+			}
+		}
+
 		if (cc == 'z')
 			spawn_shell(realold, realnew);
 	} while (!strchr("yino", cc));
-- 
1.7.5.1


Reply to: