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

[PATCH 7/7] Track common ancestors of a successful conffiledb merge



When a merge is successful, store the common ancestor of the merge (the
pristine version of the conffile from the previous package version) in
a special location. While this may not have an immediate benefit, it poses
the oppurtunity for further merge resolution options in future merges.

When the admin later chooses to install the package-shipped version of
a conffile, or when the package is purged, this information is removed
from the conffiledb as it is no longer relevant.

This commit also contains updates to the conffiledb unit tests, covering
the new merge parent tracking functionality.
---
 lib/dpkg/test/t-conffiledb.c |   52 +++++++++++++++++++++-
 src/conffiledb.c             |   99 ++++++++++++++++++++++++++++++++++++++++-
 src/conffiledb.h             |   12 +++++-
 src/configure.c              |    4 ++
 src/remove.c                 |    1 +
 5 files changed, 162 insertions(+), 6 deletions(-)

diff --git a/lib/dpkg/test/t-conffiledb.c b/lib/dpkg/test/t-conffiledb.c
index 8b07e5f..2c32322 100644
--- a/lib/dpkg/test/t-conffiledb.c
+++ b/lib/dpkg/test/t-conffiledb.c
@@ -40,8 +40,8 @@ void ensure_pathname_nonexisting(const char *pathname);
  */
 char *admindir;
 
-/* Convenience pointer used for inspection of conffiledb contents */
-char *conffiledbdir;
+/* Convenience pointers used for inspection of conffiledb contents */
+char *conffiledbdir, *mergeparentdir;
 
 /**
  * Flags for controlling output settings in debug().
@@ -74,6 +74,10 @@ setup(void)
 	if (mkdir(conffiledbdir, 0777))
 		ohshite("mkdir failed in setup");
 
+	mergeparentdir = m_malloc(strlen(conffiledbdir) + 1 +
+	                          strlen("/foopkg/merge:parent") + 1);
+	sprintf(mergeparentdir, "%s/%s", conffiledbdir, "/foopkg/merge:parent");
+
 	/* pkg_a is in the unpack phase of being upgraded */
 	pkg_a.name = m_strdup("foopkg");
 	pkg_a.installed.version.epoch = 1;
@@ -135,6 +139,7 @@ teardown(void)
 	ensure_pathname_nonexisting(admindir);
 	free(admindir);
 	free(conffiledbdir);
+	free(mergeparentdir);
 }
 
 /**
@@ -334,6 +339,48 @@ test_conffiledb_merge_failure(void)
 	free(read_buf);
 }
 
+/**
+ * Test that the recording of merge parents works correctly.
+ */
+static void
+test_conffiledb_record_mergeparent(void)
+{
+	int install_fd;
+	char *content_local = m_strdup("one\nfoo\nthree\nfour\n");
+	char *read_buf = m_malloc(64); /* just needs to be bigger than above */
+	int fake_sz_local = strlen(content_local);
+	struct stat stat_buf;
+	struct varbuf expected_parent_location = VARBUF_INIT;
+
+	/* install an arbitrary conffile and enter it into the conffiledb */
+	install_fd = open(pkg_a_cnff_1, O_RDWR|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 (lseek(install_fd, 0, SEEK_SET) == -1)
+		ohshite("install seek failed");
+	conffiledb_register_new_conffile(&pkg_a, pkg_a_cnff_1, install_fd,
+	                                 fake_sz_local);
+
+	/* record the merge and ensure it shows up correctly */
+	varbufprintf(&expected_parent_location, "%s/%s", mergeparentdir,
+	             pkg_a_cnff_1_md5);
+	test_pass(stat(expected_parent_location.buf, &stat_buf) == -1);
+	conffiledb_set_merge_parent(&pkg_a, &pkg_a.available.version,
+	                            pkg_a_cnff_1);
+	test_pass(stat(expected_parent_location.buf, &stat_buf) == 0);
+	test_pass(stat_buf.st_size == fake_sz_local);
+
+	/* double check that recording the removal also works */
+	conffiledb_remove_merge_parent(&pkg_a, pkg_a_cnff_1);
+	test_pass(stat(expected_parent_location.buf, &stat_buf) == -1);
+
+	free(content_local);
+	free(read_buf);
+	varbuffree(&expected_parent_location);
+}
+
 static void
 test(void)
 {
@@ -343,6 +390,7 @@ test(void)
 	test_conffiledb_storage();
 	test_conffiledb_merge();
 	test_conffiledb_merge_failure();
+	test_conffiledb_record_mergeparent();
 
 	teardown();
 }
diff --git a/src/conffiledb.c b/src/conffiledb.c
index baaf7ba..ef56939 100644
--- a/src/conffiledb.c
+++ b/src/conffiledb.c
@@ -43,6 +43,14 @@
 #include "conffiledb.h"
 
 #define CONFFILEDBDIRROOT "conffiles" /**< @ref conffiledb root location */
+/**
+ * The subdirectory for storing merge parent versions of files.
+ *
+ * Note that this is required to be invalid with respect to policy-compliant
+ * version strings, in order to avoid a namespace clash with versions in the
+ * same directory for any given package.
+ */
+#define MERGEPARENTDIR "merge:parent"
 
 static char *conffiledb_path_cversion(const struct pkginfo *pkg,
                                       const char *version, const char *path);
@@ -95,7 +103,7 @@ conffiledb_path(const struct pkginfo *pkg,
  * conffiledb_path function. It is seperated from the main function
  * to allow for some internal use where a path needs to be generated
  * but not from a valid struct versionrevision (i.e. any special "merge"
- * or "dynamic" versions).
+ * or "dynamic" versions, like #MERGEPARENTDIR).
  *
  * For more information on the parameters, see conffiledb_path.
  *
@@ -142,7 +150,9 @@ conffiledb_path_cversion(const struct pkginfo *pkg, const char *version,
  * Ensure that a particular conffiledb dir exists.
  *
  * @param pkg The package owning the conffiledb dir.
- * @param version Which package version to reference.
+ * @param version Which package version to reference. If NULL, the containing
+ *        directory and any other necessary supporting files/dirs are created
+ *        instead (i.e. the merge parent directory).
  */
 static void
 conffiledb_ensure_db(const struct pkginfo *pkg,
@@ -150,10 +160,19 @@ conffiledb_ensure_db(const struct pkginfo *pkg,
 {
 	struct stat s;
 	int i;
+	struct varbuf merge_parent_fname = VARBUF_INIT;
 	char *dbdirs[2];
 
 	dbdirs[0] = conffiledb_path(pkg, NULL, NULL);
-	dbdirs[1] = conffiledb_path(pkg, version, NULL);
+	if (version) {
+		dbdirs[1] = conffiledb_path(pkg, version, NULL);
+	} else {
+		/* note: the varbuf contents are later free()'d so no need
+		 *       to varbuffree merge_parent_fname */
+		varbufprintf(&merge_parent_fname, "%s/%s", dbdirs[0],
+		             MERGEPARENTDIR);
+		dbdirs[1] = merge_parent_fname.buf;
+	}
 
 	/* ensure each directory exists while reconstructing the path */
 	for (i = 0; i < 2; i++) {
@@ -265,6 +284,80 @@ conffiledb_remove(const struct pkginfo *pkg,
 }
 
 /**
+ * Record the common ancestor of a successful merge.
+ *
+ * Assuming pristine v1 conffile was edited locally by the admin, and
+ * then successfully merged with pristine v2 conffile, we then want to
+ * record v1 as a "common ancestor" of the local file and v2, which
+ * may be helpful in future merges.
+ *
+ * @param pkg The package owning the conffile.
+ * @param version The package version of the "common ancestor".
+ * @param path The path to the conffile.
+ */
+void
+conffiledb_set_merge_parent(const struct pkginfo *pkg,
+                            const struct versionrevision *version,
+                            const char *path)
+{
+	char *src_fname, *dst_fname, quoted_filename[256];
+	struct varbuf tmp_fname_buf = VARBUF_INIT;
+	int src_fd, tmp_fd;
+
+	conffiledb_ensure_db(pkg, NULL);
+
+	src_fname = conffiledb_path(pkg, version, path);
+	dst_fname = conffiledb_path_cversion(pkg, MERGEPARENTDIR, path);
+	varbufprintf(&tmp_fname_buf, "%s%s", dst_fname, DPKGTEMPEXT);
+
+	src_fd = open(src_fname, O_RDONLY);
+	if (src_fd == -1)
+		ohshite(_("open '%s' failed"), src_fname);
+
+	tmp_fd = open(tmp_fname_buf.buf, O_CREAT|O_TRUNC|O_WRONLY, 0600);
+	if (tmp_fd == -1)
+		ohshite(_("open '%s' failed"), tmp_fname_buf.buf);
+
+	fd_fd_copy(src_fd, tmp_fd, -1,
+	           _("recording conffiledb merge ancestor for '%.255s'"),
+	           path_quote_filename(quoted_filename, src_fname, 256));
+
+	if (close(src_fd) == -1)
+		ohshite(_("close '%s' failed"), src_fname);
+	if (close(tmp_fd) == -1)
+		ohshite(_("close '%s' failed"), tmp_fname_buf.buf);
+
+	if (rename(tmp_fname_buf.buf, dst_fname) == -1)
+		ohshite(_("rename '%s' failed"), tmp_fname_buf.buf);
+
+	free(src_fname);
+	free(dst_fname);
+	varbuffree(&tmp_fname_buf);
+}
+
+/**
+ * Remove the common ancestor of a previous merge.
+ *
+ * This function provides a way to remove the ancestry information
+ * for merges that are no longer relevant (because the admin has
+ * installed the dist version of a conffile in a later upgrade, for
+ * example).
+ *
+ * @param pkg The package owning the conffile.
+ * @param path The path to the conffile.
+ */
+void
+conffiledb_remove_merge_parent(const struct pkginfo *pkg, const char *path)
+{
+	char *pkg_dir;
+
+	pkg_dir = conffiledb_path_cversion(pkg, MERGEPARENTDIR, path);
+	ensure_pathname_nonexisting(pkg_dir);
+
+	free(pkg_dir);
+}
+
+/**
  * Attempt to automagically merge a 3-way delta for a conffile.
  *
  * Using the pristine version of the conffile a merge is attempted via
diff --git a/src/conffiledb.h b/src/conffiledb.h
index d0db9de..e39522c 100644
--- a/src/conffiledb.h
+++ b/src/conffiledb.h
@@ -16,7 +16,10 @@
  *
  * where:
  * - @b \<pkg\> is the conffile database directory for an individual package.
- * - @b \<version\> is the version of the package owning the conffile.
+ * - @b \<version\> is the version of the package owning the conffile, or in
+ *   some special cases a "pseudo-version" to track a version of the conffile
+ *   not associated with a particular package version (such as a merge
+ *   ancestor or other dynamically registered file).
  * - @b \<hash\> is the md5 hash of the location of a package's conffile. A
  *   hash is used to keep a flat and balanced directory structure, and has
  *   the added benefit of a simpler implementation.
@@ -71,4 +74,11 @@ void conffiledb_remove(const struct pkginfo *pkg,
 
 int conffiledb_automerge(const struct pkginfo *pkg, const char *path);
 
+void conffiledb_set_merge_parent(const struct pkginfo *pkg,
+                                 const struct versionrevision *version,
+                                 const char *path);
+
+void conffiledb_remove_merge_parent(const struct pkginfo *pkg,
+                                    const char *path);
+
 #endif /* DPKG_CONFFILEDB_H */
diff --git a/src/configure.c b/src/configure.c
index 00347fb..bf1f3d4 100644
--- a/src/configure.c
+++ b/src/configure.c
@@ -193,6 +193,7 @@ deferred_configure_conffile(struct pkginfo *pkg, struct conffile *conff)
 	case cfo_install:
 		printf(_("Installing new version of config file %s ...\n"),
 		       usenode->name);
+		conffiledb_remove_merge_parent(pkg, cdr.buf);
 	case cfo_newconff:
 		strcpy(cdr2rest, DPKGNEWEXT);
 		trig_file_activate(usenode, pkg);
@@ -723,6 +724,9 @@ promptconfaction(struct pkginfo *pkg, const char *cfgfile,
 				/* so let's just pretend they said "keep my
 				 * version" ;) */
 				cc = 'n';
+				conffiledb_set_merge_parent(pkg,
+				                            &pkg->configversion,
+				                            cfgfile);
 			} else {
 				fprintf(stderr, _("failed.\n"));
 			}
diff --git a/src/remove.c b/src/remove.c
index 909f546..8d59605 100644
--- a/src/remove.c
+++ b/src/remove.c
@@ -435,6 +435,7 @@ static void removal_bulk_remove_configfiles(struct pkginfo *pkg) {
       if (unlink(fnvb.buf) && errno != ENOENT && errno != ENOTDIR)
         ohshite(_("cannot remove old config file `%.250s' (= `%.250s')"),
                 conff->name, fnvb.buf);
+      conffiledb_remove_merge_parent(pkg, conff->name);
       p= strrchr(fnvb.buf,'/'); if (!p) continue;
       *p = '\0';
       varbufreset(&removevb);
-- 
1.6.5.3


Reply to: