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 <dsto...@microsoft.com>
---
 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

Reply via email to