Subject: [PATCH v4 08/13] commit-graph: implement --delete-expired

From: Derrick Stolee <[email-masked]>
Date: Mon, 19 Feb 2018 13:53:21 -0500

Teach git-commit-graph to delete the .graph files that are siblings of a
newly-written graph file, except for the file referenced by 'graph-latest'
at the beginning of the process and the newly-written file. If we fail to
delete a graph file, only report a warning because another git process may
be using that file. In a multi-process environment, we expect the previoius
graph file to be used by a concurrent process, so we do not delete it to
avoid race conditions.

Signed-off-by: Derrick Stolee <[email-masked]>
---
 Documentation/git-commit-graph.txt | 11 +++++--
 builtin/commit-graph.c             | 61 ++++++++++++++++++++++++++++++++++++--
 commit-graph.c                     | 23 ++++++++++++++
 commit-graph.h                     |  1 +
 t/t5318-commit-graph.sh            |  7 +++--
 5 files changed, 96 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-commit-graph.txt b/Documentation/git-commit-graph.txt
index dc948c5..b9b4031 100644
--- a/Documentation/git-commit-graph.txt
+++ b/Documentation/git-commit-graph.txt
@@ -37,6 +37,11 @@ resulting filename.
 +
 With `--set-latest` option, update the graph-latest file to point
 to the written graph file.
++
+With the `--delete-expired` option, delete the graph files in the pack
+directory that are not referred to by the graph-latest file. To avoid race
+conditions, do not delete the file previously referred to by the
+graph-latest file if it is updated by the `--set-latest` option.
 
 'read'::
 
@@ -56,11 +61,11 @@ EXAMPLES
 $ git commit-graph write
 ------------------------------------------------
 
-* Write a graph file for the packed commits in your local .git folder
-* and update graph-latest.
+* Write a graph file for the packed commits in your local .git folder,
+* update graph-latest, and delete stale graph files.
 +
 ------------------------------------------------
-$ git commit-graph write --set-latest
+$ git commit-graph write --set-latest --delete-expired
 ------------------------------------------------
 
 * Read basic information from a graph file.
diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c
index bf86172..fd99169 100644
--- a/builtin/commit-graph.c
+++ b/builtin/commit-graph.c
@@ -8,7 +8,7 @@
 static char const * const builtin_commit_graph_usage[] = {
 	N_("git commit-graph [--object-dir <objdir>]"),
 	N_("git commit-graph read [--object-dir <objdir>] [--file=<hash>]"),
-	N_("git commit-graph write [--object-dir <objdir>] [--set-latest]"),
+	N_("git commit-graph write [--object-dir <objdir>] [--set-latest] [--delete-expired]"),
 	NULL
 };
 
@@ -18,7 +18,7 @@ static const char * const builtin_commit_graph_read_usage[] = {
 };
 
 static const char * const builtin_commit_graph_write_usage[] = {
-	N_("git commit-graph write [--object-dir <objdir>] [--set-latest]"),
+	N_("git commit-graph write [--object-dir <objdir>] [--set-latest] [--delete-expired]"),
 	NULL
 };
 
@@ -26,6 +26,7 @@ static struct opts_commit_graph {
 	const char *obj_dir;
 	const char *graph_file;
 	int set_latest;
+	int delete_expired;
 } opts;
 
 static int graph_read(int argc, const char **argv)
@@ -98,9 +99,56 @@ static void set_latest_file(const char *obj_dir, const char *graph_file)
 	commit_lock_file(&lk);
 }
 
+/*
+ * To avoid race conditions and deleting graph files that are being
+ * used by other processes, look inside a pack directory for all files
+ * of the form "graph-<hash>.graph" that do not match the old or new
+ * graph hashes and delete them.
+ */
+static void do_delete_expired(const char *obj_dir,
+			      const char *old_graph_name,
+			      const char *new_graph_name)
+{
+	DIR *dir;
+	struct dirent *de;
+	int dirnamelen;
+	struct strbuf path = STRBUF_INIT;
+
+	strbuf_addf(&path, "%s/info", obj_dir);
+	dir = opendir(path.buf);
+	if (!dir) {
+		if (errno != ENOENT)
+			error_errno("unable to open object pack directory: %s",
+				    obj_dir);
+		return;
+	}
+
+	strbuf_addch(&path, '/');
+	dirnamelen = path.len;
+	while ((de = readdir(dir)) != NULL) {
+		size_t base_len;
+
+		if (is_dot_or_dotdot(de->d_name))
+			continue;
+
+		strbuf_setlen(&path, dirnamelen);
+		strbuf_addstr(&path, de->d_name);
+
+		base_len = path.len;
+		if (strip_suffix_mem(path.buf, &base_len, ".graph") &&
+		    strcmp(new_graph_name, de->d_name) &&
+		    (!old_graph_name || strcmp(old_graph_name, de->d_name)) &&
+		    remove_path(path.buf))
+			die("failed to remove path %s", path.buf);
+	}
+
+	strbuf_release(&path);
+}
+
 static int graph_write(int argc, const char **argv)
 {
 	char *graph_name;
+	char *old_graph_name;
 
 	static struct option builtin_commit_graph_write_options[] = {
 		{ OPTION_STRING, 'o', "object-dir", &opts.obj_dir,
@@ -108,6 +156,8 @@ static int graph_write(int argc, const char **argv)
 			N_("The object directory to store the graph") },
 		OPT_BOOL('u', "set-latest", &opts.set_latest,
 			N_("update graph-head to written graph file")),
+		OPT_BOOL('d', "delete-expired", &opts.delete_expired,
+			N_("delete expired head graph file")),
 		OPT_END(),
 	};
 
@@ -118,12 +168,19 @@ static int graph_write(int argc, const char **argv)
 	if (!opts.obj_dir)
 		opts.obj_dir = get_object_directory();
 
+	old_graph_name = get_graph_latest_contents(opts.obj_dir);
+
 	graph_name = write_commit_graph(opts.obj_dir);
 
 	if (graph_name) {
 		if (opts.set_latest)
 			set_latest_file(opts.obj_dir, graph_name);
 
+		if (opts.delete_expired)
+			do_delete_expired(opts.obj_dir,
+					  old_graph_name,
+					  graph_name);
+
 		printf("%s\n", graph_name);
 		FREE_AND_NULL(graph_name);
 	}
diff --git a/commit-graph.c b/commit-graph.c
index 5ee0805..c8fb38f 100644
--- a/commit-graph.c
+++ b/commit-graph.c
@@ -45,6 +45,29 @@ char *get_graph_latest_filename(const char *obj_dir)
 	return strbuf_detach(&fname, 0);
 }
 
+char *get_graph_latest_contents(const char *obj_dir)
+{
+	struct strbuf graph_file = STRBUF_INIT;
+	char *fname;
+	FILE *f;
+	char buf[64];
+
+	fname = get_graph_latest_filename(obj_dir);
+	f = fopen(fname, "r");
+	FREE_AND_NULL(fname);
+
+	if (!f)
+		return 0;
+
+	while (!feof(f)) {
+		if (fgets(buf, sizeof(buf), f))
+			strbuf_addstr(&graph_file, buf);
+	}
+
+	fclose(f);
+	return strbuf_detach(&graph_file, NULL);
+}
+
 static struct commit_graph *alloc_commit_graph(void)
 {
 	struct commit_graph *g = xmalloc(sizeof(*g));
diff --git a/commit-graph.h b/commit-graph.h
index ae24b3a..56215ad 100644
--- a/commit-graph.h
+++ b/commit-graph.h
@@ -4,6 +4,7 @@
 #include "git-compat-util.h"
 
 extern char *get_graph_latest_filename(const char *obj_dir);
+extern char *get_graph_latest_contents(const char *obj_dir);
 
 struct commit_graph {
 	int graph_fd;
diff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh
index cad9d90..1d5ec7d 100755
--- a/t/t5318-commit-graph.sh
+++ b/t/t5318-commit-graph.sh
@@ -117,8 +117,10 @@ test_expect_success 'Add one more commit' '
 # 1
 
 test_expect_success 'write graph with new commit' '
-	graph3=$(git commit-graph write --set-latest) &&
+	graph3=$(git commit-graph write --set-latest --delete-expired) &&
 	test_path_is_file $objdir/info/$graph3 &&
+	test_path_is_file $objdir/info/$graph2 &&
+	test_path_is_missing $objdir/info/$graph1 &&
 	test_path_is_file $objdir/info/graph-latest &&
 	printf $graph3 >expect &&
 	test_cmp expect $objdir/info/graph-latest &&
@@ -128,8 +130,9 @@ test_expect_success 'write graph with new commit' '
 '
 
 test_expect_success 'write graph with nothing new' '
-	graph4=$(git commit-graph write --set-latest) &&
+	graph4=$(git commit-graph write --set-latest --delete-expired) &&
 	test_path_is_file $objdir/info/$graph4 &&
+	test_path_is_missing $objdir/info/$graph2 &&
 	printf $graph3 >expect &&
 	printf $graph4 >output &&
 	test_cmp expect output &&
-- 
2.7.4