reflog --expire-unreachable: avoid merge-base computation
[gitweb.git] / builtin-reflog.c
index d95f515f2e32aa71c3a09fe5ed9486f352713360..9792090d8680f9fe09004d8f330d8b7debeda1ac 100644 (file)
@@ -36,6 +36,8 @@ struct expire_reflog_cb {
        FILE *newlog;
        const char *ref;
        struct commit *ref_commit;
+       struct commit_list *mark_list;
+       unsigned long mark_limit;
        struct cmd_reflog_expire_cb *cmd;
        unsigned char last_kept_sha1[20];
 };
@@ -52,6 +54,7 @@ struct collect_reflog_cb {
 
 #define INCOMPLETE     (1u<<10)
 #define STUDYING       (1u<<11)
+#define REACHABLE      (1u<<12)
 
 static int tree_is_complete(const unsigned char *sha1)
 {
@@ -209,6 +212,80 @@ static int keep_entry(struct commit **it, unsigned char *sha1)
        return 1;
 }
 
+/*
+ * Starting from commits in the cb->mark_list, mark commits that are
+ * reachable from them.  Stop the traversal at commits older than
+ * the expire_limit and queue them back, so that the caller can call
+ * us again to restart the traversal with longer expire_limit.
+ */
+static void mark_reachable(struct expire_reflog_cb *cb)
+{
+       struct commit *commit;
+       struct commit_list *pending;
+       unsigned long expire_limit = cb->mark_limit;
+       struct commit_list *leftover = NULL;
+
+       for (pending = cb->mark_list; pending; pending = pending->next)
+               pending->item->object.flags &= ~REACHABLE;
+
+       pending = cb->mark_list;
+       while (pending) {
+               struct commit_list *entry = pending;
+               struct commit_list *parent;
+               pending = entry->next;
+               commit = entry->item;
+               free(entry);
+               if (commit->object.flags & REACHABLE)
+                       continue;
+               if (parse_commit(commit))
+                       continue;
+               commit->object.flags |= REACHABLE;
+               if (commit->date < expire_limit) {
+                       commit_list_insert(commit, &leftover);
+                       continue;
+               }
+               commit->object.flags |= REACHABLE;
+               parent = commit->parents;
+               while (parent) {
+                       commit = parent->item;
+                       parent = parent->next;
+                       if (commit->object.flags & REACHABLE)
+                               continue;
+                       commit_list_insert(commit, &pending);
+               }
+       }
+       cb->mark_list = leftover;
+}
+
+static int unreachable(struct expire_reflog_cb *cb, struct commit *commit, unsigned char *sha1)
+{
+       /*
+        * We may or may not have the commit yet - if not, look it
+        * up using the supplied sha1.
+        */
+       if (!commit) {
+               if (is_null_sha1(sha1))
+                       return 0;
+
+               commit = lookup_commit_reference_gently(sha1, 1);
+
+               /* Not a commit -- keep it */
+               if (!commit)
+                       return 0;
+       }
+
+       /* Reachable from the current ref?  Don't prune. */
+       if (commit->object.flags & REACHABLE)
+               return 0;
+
+       if (cb->mark_list && cb->mark_limit) {
+               cb->mark_limit = 0; /* dig down to the root */
+               mark_reachable(cb);
+       }
+
+       return !(commit->object.flags & REACHABLE);
+}
+
 static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
                const char *email, unsigned long timestamp, int tz,
                const char *message, void *cb_data)
@@ -230,12 +307,7 @@ static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
        if (timestamp < cb->cmd->expire_unreachable) {
                if (!cb->ref_commit)
                        goto prune;
-               if (!old && !is_null_sha1(osha1))
-                       old = lookup_commit_reference_gently(osha1, 1);
-               if (!new && !is_null_sha1(nsha1))
-                       new = lookup_commit_reference_gently(nsha1, 1);
-               if ((old && !in_merge_bases(old, &cb->ref_commit, 1)) ||
-                   (new && !in_merge_bases(new, &cb->ref_commit, 1)))
+               if (unreachable(cb, old, osha1) || unreachable(cb, new, nsha1))
                        goto prune;
        }
 
@@ -288,7 +360,15 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,
        cb.ref_commit = lookup_commit_reference_gently(sha1, 1);
        cb.ref = ref;
        cb.cmd = cmd;
+       if (cb.ref_commit) {
+               cb.mark_list = NULL;
+               commit_list_insert(cb.ref_commit, &cb.mark_list);
+               cb.mark_limit = cmd->expire_total;
+               mark_reachable(&cb);
+       }
        for_each_reflog_ent(ref, expire_reflog_ent, &cb);
+       if (cb.ref_commit)
+               clear_commit_marks(cb.ref_commit, REACHABLE);
  finish:
        if (cb.newlog) {
                if (fclose(cb.newlog)) {
@@ -298,7 +378,7 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,
                } else if (cmd->updateref &&
                        (write_in_full(lock->lock_fd,
                                sha1_to_hex(cb.last_kept_sha1), 40) != 40 ||
-                        write_in_full(lock->lock_fd, "\n", 1) != 1 ||
+                        write_str_in_full(lock->lock_fd, "\n") != 1 ||
                         close_ref(lock) < 0)) {
                        status |= error("Couldn't write %s",
                                lock->lk->filename);
@@ -630,10 +710,13 @@ static int cmd_reflog_delete(int argc, const char **argv, const char *prefix)
  */
 
 static const char reflog_usage[] =
-"git reflog (expire | ...)";
+"git reflog [ show | expire | delete ]";
 
 int cmd_reflog(int argc, const char **argv, const char *prefix)
 {
+       if (argc > 1 && !strcmp(argv[1], "-h"))
+               usage(reflog_usage);
+
        /* With no command, we default to showing it. */
        if (argc < 2 || *argv[1] == '-')
                return cmd_log_reflog(argc, argv, prefix);