]> git.ipfire.org Git - thirdparty/git.git/commitdiff
reflog: libify delete reflog function and helpers
authorJohn Cai <johncai86@gmail.com>
Wed, 2 Mar 2022 22:27:23 +0000 (22:27 +0000)
committerJunio C Hamano <gitster@pobox.com>
Wed, 2 Mar 2022 23:24:47 +0000 (15:24 -0800)
Currently stash shells out to reflog in order to delete refs. In an
effort to reduce how much we shell out to a subprocess, libify the
functionality that stash needs into reflog.c.

Add a reflog_delete function that is pretty much the logic in the while
loop in builtin/reflog.c cmd_reflog_delete(). This is a function that
builtin/reflog.c and builtin/stash.c can both call.

Also move functions needed by reflog_delete and export them.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Makefile
builtin/reflog.c
object.h
reflog.c [new file with mode: 0644]
reflog.h [new file with mode: 0644]

index 6f0b4b775fece6acd20348e318a8cff3e254bcb2..876d4dfd6cb6e5d517a9f1608546ff55ce92d571 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -989,6 +989,7 @@ LIB_OBJS += rebase-interactive.o
 LIB_OBJS += rebase.o
 LIB_OBJS += ref-filter.o
 LIB_OBJS += reflog-walk.o
+LIB_OBJS += reflog.o
 LIB_OBJS += refs.o
 LIB_OBJS += refs/debug.o
 LIB_OBJS += refs/files-backend.o
index c8f2b318731991a008ef5a949319305798a0f204..ce1c45b7b60cbd8c59877d6a2e5cde4fbe53a3c3 100644 (file)
@@ -1,16 +1,9 @@
 #include "builtin.h"
 #include "config.h"
-#include "lockfile.h"
-#include "object-store.h"
-#include "repository.h"
-#include "commit.h"
-#include "refs.h"
-#include "dir.h"
-#include "tree-walk.h"
-#include "diff.h"
 #include "revision.h"
 #include "reachable.h"
 #include "worktree.h"
+#include "reflog.h"
 
 static const char reflog_exists_usage[] =
 N_("git reflog exists <ref>");
@@ -18,404 +11,11 @@ N_("git reflog exists <ref>");
 static timestamp_t default_reflog_expire;
 static timestamp_t default_reflog_expire_unreachable;
 
-struct cmd_reflog_expire_cb {
-       int stalefix;
-       int explicit_expiry;
-       timestamp_t expire_total;
-       timestamp_t expire_unreachable;
-       int recno;
-};
-
-struct expire_reflog_policy_cb {
-       enum {
-               UE_NORMAL,
-               UE_ALWAYS,
-               UE_HEAD
-       } unreachable_expire_kind;
-       struct commit_list *mark_list;
-       unsigned long mark_limit;
-       struct cmd_reflog_expire_cb cmd;
-       struct commit *tip_commit;
-       struct commit_list *tips;
-       unsigned int dry_run:1;
-};
-
 struct worktree_reflogs {
        struct worktree *worktree;
        struct string_list reflogs;
 };
 
-/* Remember to update object flag allocation in object.h */
-#define INCOMPLETE     (1u<<10)
-#define STUDYING       (1u<<11)
-#define REACHABLE      (1u<<12)
-
-static int tree_is_complete(const struct object_id *oid)
-{
-       struct tree_desc desc;
-       struct name_entry entry;
-       int complete;
-       struct tree *tree;
-
-       tree = lookup_tree(the_repository, oid);
-       if (!tree)
-               return 0;
-       if (tree->object.flags & SEEN)
-               return 1;
-       if (tree->object.flags & INCOMPLETE)
-               return 0;
-
-       if (!tree->buffer) {
-               enum object_type type;
-               unsigned long size;
-               void *data = read_object_file(oid, &type, &size);
-               if (!data) {
-                       tree->object.flags |= INCOMPLETE;
-                       return 0;
-               }
-               tree->buffer = data;
-               tree->size = size;
-       }
-       init_tree_desc(&desc, tree->buffer, tree->size);
-       complete = 1;
-       while (tree_entry(&desc, &entry)) {
-               if (!has_object_file(&entry.oid) ||
-                   (S_ISDIR(entry.mode) && !tree_is_complete(&entry.oid))) {
-                       tree->object.flags |= INCOMPLETE;
-                       complete = 0;
-               }
-       }
-       free_tree_buffer(tree);
-
-       if (complete)
-               tree->object.flags |= SEEN;
-       return complete;
-}
-
-static int commit_is_complete(struct commit *commit)
-{
-       struct object_array study;
-       struct object_array found;
-       int is_incomplete = 0;
-       int i;
-
-       /* early return */
-       if (commit->object.flags & SEEN)
-               return 1;
-       if (commit->object.flags & INCOMPLETE)
-               return 0;
-       /*
-        * Find all commits that are reachable and are not marked as
-        * SEEN.  Then make sure the trees and blobs contained are
-        * complete.  After that, mark these commits also as SEEN.
-        * If some of the objects that are needed to complete this
-        * commit are missing, mark this commit as INCOMPLETE.
-        */
-       memset(&study, 0, sizeof(study));
-       memset(&found, 0, sizeof(found));
-       add_object_array(&commit->object, NULL, &study);
-       add_object_array(&commit->object, NULL, &found);
-       commit->object.flags |= STUDYING;
-       while (study.nr) {
-               struct commit *c;
-               struct commit_list *parent;
-
-               c = (struct commit *)object_array_pop(&study);
-               if (!c->object.parsed && !parse_object(the_repository, &c->object.oid))
-                       c->object.flags |= INCOMPLETE;
-
-               if (c->object.flags & INCOMPLETE) {
-                       is_incomplete = 1;
-                       break;
-               }
-               else if (c->object.flags & SEEN)
-                       continue;
-               for (parent = c->parents; parent; parent = parent->next) {
-                       struct commit *p = parent->item;
-                       if (p->object.flags & STUDYING)
-                               continue;
-                       p->object.flags |= STUDYING;
-                       add_object_array(&p->object, NULL, &study);
-                       add_object_array(&p->object, NULL, &found);
-               }
-       }
-       if (!is_incomplete) {
-               /*
-                * make sure all commits in "found" array have all the
-                * necessary objects.
-                */
-               for (i = 0; i < found.nr; i++) {
-                       struct commit *c =
-                               (struct commit *)found.objects[i].item;
-                       if (!tree_is_complete(get_commit_tree_oid(c))) {
-                               is_incomplete = 1;
-                               c->object.flags |= INCOMPLETE;
-                       }
-               }
-               if (!is_incomplete) {
-                       /* mark all found commits as complete, iow SEEN */
-                       for (i = 0; i < found.nr; i++)
-                               found.objects[i].item->flags |= SEEN;
-               }
-       }
-       /* clear flags from the objects we traversed */
-       for (i = 0; i < found.nr; i++)
-               found.objects[i].item->flags &= ~STUDYING;
-       if (is_incomplete)
-               commit->object.flags |= INCOMPLETE;
-       else {
-               /*
-                * If we come here, we have (1) traversed the ancestry chain
-                * from the "commit" until we reach SEEN commits (which are
-                * known to be complete), and (2) made sure that the commits
-                * encountered during the above traversal refer to trees that
-                * are complete.  Which means that we know *all* the commits
-                * we have seen during this process are complete.
-                */
-               for (i = 0; i < found.nr; i++)
-                       found.objects[i].item->flags |= SEEN;
-       }
-       /* free object arrays */
-       object_array_clear(&study);
-       object_array_clear(&found);
-       return !is_incomplete;
-}
-
-static int keep_entry(struct commit **it, struct object_id *oid)
-{
-       struct commit *commit;
-
-       if (is_null_oid(oid))
-               return 1;
-       commit = lookup_commit_reference_gently(the_repository, oid, 1);
-       if (!commit)
-               return 0;
-
-       /*
-        * Make sure everything in this commit exists.
-        *
-        * We have walked all the objects reachable from the refs
-        * and cache earlier.  The commits reachable by this commit
-        * must meet SEEN commits -- and then we should mark them as
-        * SEEN as well.
-        */
-       if (!commit_is_complete(commit))
-               return 0;
-       *it = commit;
-       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_policy_cb *cb)
-{
-       struct commit_list *pending;
-       timestamp_t 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 *parent;
-               struct commit *commit = pop_commit(&pending);
-               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_policy_cb *cb, struct commit *commit, struct object_id *oid)
-{
-       /*
-        * We may or may not have the commit yet - if not, look it
-        * up using the supplied sha1.
-        */
-       if (!commit) {
-               if (is_null_oid(oid))
-                       return 0;
-
-               commit = lookup_commit_reference_gently(the_repository, oid,
-                                                       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);
-}
-
-/*
- * Return true iff the specified reflog entry should be expired.
- */
-static int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,
-                                   const char *email, timestamp_t timestamp, int tz,
-                                   const char *message, void *cb_data)
-{
-       struct expire_reflog_policy_cb *cb = cb_data;
-       struct commit *old_commit, *new_commit;
-
-       if (timestamp < cb->cmd.expire_total)
-               return 1;
-
-       old_commit = new_commit = NULL;
-       if (cb->cmd.stalefix &&
-           (!keep_entry(&old_commit, ooid) || !keep_entry(&new_commit, noid)))
-               return 1;
-
-       if (timestamp < cb->cmd.expire_unreachable) {
-               switch (cb->unreachable_expire_kind) {
-               case UE_ALWAYS:
-                       return 1;
-               case UE_NORMAL:
-               case UE_HEAD:
-                       if (unreachable(cb, old_commit, ooid) || unreachable(cb, new_commit, noid))
-                               return 1;
-                       break;
-               }
-       }
-
-       if (cb->cmd.recno && --(cb->cmd.recno) == 0)
-               return 1;
-
-       return 0;
-}
-
-static int should_expire_reflog_ent_verbose(struct object_id *ooid,
-                                           struct object_id *noid,
-                                           const char *email,
-                                           timestamp_t timestamp, int tz,
-                                           const char *message, void *cb_data)
-{
-       struct expire_reflog_policy_cb *cb = cb_data;
-       int expire;
-
-       expire = should_expire_reflog_ent(ooid, noid, email, timestamp, tz,
-                                         message, cb);
-
-       if (!expire)
-               printf("keep %s", message);
-       else if (cb->dry_run)
-               printf("would prune %s", message);
-       else
-               printf("prune %s", message);
-
-       return expire;
-}
-
-static int push_tip_to_list(const char *refname, const struct object_id *oid,
-                           int flags, void *cb_data)
-{
-       struct commit_list **list = cb_data;
-       struct commit *tip_commit;
-       if (flags & REF_ISSYMREF)
-               return 0;
-       tip_commit = lookup_commit_reference_gently(the_repository, oid, 1);
-       if (!tip_commit)
-               return 0;
-       commit_list_insert(tip_commit, list);
-       return 0;
-}
-
-static int is_head(const char *refname)
-{
-       switch (ref_type(refname)) {
-       case REF_TYPE_OTHER_PSEUDOREF:
-       case REF_TYPE_MAIN_PSEUDOREF:
-               if (parse_worktree_ref(refname, NULL, NULL, &refname))
-                       BUG("not a worktree ref: %s", refname);
-               break;
-       default:
-               break;
-       }
-       return !strcmp(refname, "HEAD");
-}
-
-static void reflog_expiry_prepare(const char *refname,
-                                 const struct object_id *oid,
-                                 void *cb_data)
-{
-       struct expire_reflog_policy_cb *cb = cb_data;
-       struct commit_list *elem;
-       struct commit *commit = NULL;
-
-       if (!cb->cmd.expire_unreachable || is_head(refname)) {
-               cb->unreachable_expire_kind = UE_HEAD;
-       } else {
-               commit = lookup_commit(the_repository, oid);
-               cb->unreachable_expire_kind = commit ? UE_NORMAL : UE_ALWAYS;
-       }
-
-       if (cb->cmd.expire_unreachable <= cb->cmd.expire_total)
-               cb->unreachable_expire_kind = UE_ALWAYS;
-
-       switch (cb->unreachable_expire_kind) {
-       case UE_ALWAYS:
-               return;
-       case UE_HEAD:
-               for_each_ref(push_tip_to_list, &cb->tips);
-               for (elem = cb->tips; elem; elem = elem->next)
-                       commit_list_insert(elem->item, &cb->mark_list);
-               break;
-       case UE_NORMAL:
-               commit_list_insert(commit, &cb->mark_list);
-               /* For reflog_expiry_cleanup() below */
-               cb->tip_commit = commit;
-       }
-       cb->mark_limit = cb->cmd.expire_total;
-       mark_reachable(cb);
-}
-
-static void reflog_expiry_cleanup(void *cb_data)
-{
-       struct expire_reflog_policy_cb *cb = cb_data;
-       struct commit_list *elem;
-
-       switch (cb->unreachable_expire_kind) {
-       case UE_ALWAYS:
-               return;
-       case UE_HEAD:
-               for (elem = cb->tips; elem; elem = elem->next)
-                       clear_commit_marks(elem->item, REACHABLE);
-               free_commit_list(cb->tips);
-               break;
-       case UE_NORMAL:
-               clear_commit_marks(cb->tip_commit, REACHABLE);
-               break;
-       }
-}
-
 static int collect_reflog(const char *ref, const struct object_id *oid, int unused, void *cb_data)
 {
        struct worktree_reflogs *cb = cb_data;
@@ -704,16 +304,6 @@ static int cmd_reflog_expire(int argc, const char **argv, const char *prefix)
        return status;
 }
 
-static int count_reflog_ent(struct object_id *ooid, struct object_id *noid,
-               const char *email, timestamp_t timestamp, int tz,
-               const char *message, void *cb_data)
-{
-       struct cmd_reflog_expire_cb *cb = cb_data;
-       if (!cb->expire_total || timestamp < cb->expire_total)
-               cb->recno++;
-       return 0;
-}
-
 static const char * reflog_delete_usage[] = {
        N_("git reflog delete [--rewrite] [--updateref] "
           "[--dry-run | -n] [--verbose] <refs>..."),
@@ -722,11 +312,10 @@ static const char * reflog_delete_usage[] = {
 
 static int cmd_reflog_delete(int argc, const char **argv, const char *prefix)
 {
-       struct cmd_reflog_expire_cb cmd = { 0 };
        int i, status = 0;
        unsigned int flags = 0;
        int verbose = 0;
-       reflog_expiry_should_prune_fn *should_prune_fn = should_expire_reflog_ent;
+
        const struct option options[] = {
                OPT_BIT(0, "dry-run", &flags, N_("do not actually prune any entries"),
                        EXPIRE_REFLOGS_DRY_RUN),
@@ -742,48 +331,12 @@ static int cmd_reflog_delete(int argc, const char **argv, const char *prefix)
 
        argc = parse_options(argc, argv, prefix, options, reflog_delete_usage, 0);
 
-       if (verbose)
-               should_prune_fn = should_expire_reflog_ent_verbose;
-
        if (argc < 1)
                return error(_("no reflog specified to delete"));
 
-       for (i = 0; i < argc; i++) {
-               const char *spec = strstr(argv[i], "@{");
-               char *ep, *ref;
-               int recno;
-               struct expire_reflog_policy_cb cb = {
-                       .dry_run = !!(flags & EXPIRE_REFLOGS_DRY_RUN),
-               };
-
-               if (!spec) {
-                       status |= error(_("not a reflog: %s"), argv[i]);
-                       continue;
-               }
+       for (i = 0; i < argc; i++)
+               status |= reflog_delete(argv[i], flags, verbose);
 
-               if (!dwim_log(argv[i], spec - argv[i], NULL, &ref)) {
-                       status |= error(_("no reflog for '%s'"), argv[i]);
-                       continue;
-               }
-
-               recno = strtoul(spec + 2, &ep, 10);
-               if (*ep == '}') {
-                       cmd.recno = -recno;
-                       for_each_reflog_ent(ref, count_reflog_ent, &cmd);
-               } else {
-                       cmd.expire_total = approxidate(spec + 2);
-                       for_each_reflog_ent(ref, count_reflog_ent, &cmd);
-                       cmd.expire_total = 0;
-               }
-
-               cb.cmd = cmd;
-               status |= reflog_expire(ref, flags,
-                                       reflog_expiry_prepare,
-                                       should_prune_fn,
-                                       reflog_expiry_cleanup,
-                                       &cb);
-               free(ref);
-       }
        return status;
 }
 
index cb556ab7753d5ddb01bc9421bbf490c0e684769b..a2219464c2b31043692fb9c0d224fdf0b1c2c253 100644 (file)
--- a/object.h
+++ b/object.h
@@ -75,7 +75,7 @@ struct object_array {
  * builtin/fsck.c:           0--3
  * builtin/gc.c:             0
  * builtin/index-pack.c:                                     2021
- * builtin/reflog.c:                   10--12
+ * reflog.c:                           10--12
  * builtin/show-branch.c:    0-------------------------------------------26
  * builtin/unpack-objects.c:                                 2021
  */
diff --git a/reflog.c b/reflog.c
new file mode 100644 (file)
index 0000000..333fd87
--- /dev/null
+++ b/reflog.c
@@ -0,0 +1,432 @@
+#include "cache.h"
+#include "object-store.h"
+#include "reflog.h"
+#include "refs.h"
+#include "revision.h"
+#include "worktree.h"
+
+/* Remember to update object flag allocation in object.h */
+#define INCOMPLETE     (1u<<10)
+#define STUDYING       (1u<<11)
+#define REACHABLE      (1u<<12)
+
+static int tree_is_complete(const struct object_id *oid)
+{
+       struct tree_desc desc;
+       struct name_entry entry;
+       int complete;
+       struct tree *tree;
+
+       tree = lookup_tree(the_repository, oid);
+       if (!tree)
+               return 0;
+       if (tree->object.flags & SEEN)
+               return 1;
+       if (tree->object.flags & INCOMPLETE)
+               return 0;
+
+       if (!tree->buffer) {
+               enum object_type type;
+               unsigned long size;
+               void *data = read_object_file(oid, &type, &size);
+               if (!data) {
+                       tree->object.flags |= INCOMPLETE;
+                       return 0;
+               }
+               tree->buffer = data;
+               tree->size = size;
+       }
+       init_tree_desc(&desc, tree->buffer, tree->size);
+       complete = 1;
+       while (tree_entry(&desc, &entry)) {
+               if (!has_object_file(&entry.oid) ||
+                   (S_ISDIR(entry.mode) && !tree_is_complete(&entry.oid))) {
+                       tree->object.flags |= INCOMPLETE;
+                       complete = 0;
+               }
+       }
+       free_tree_buffer(tree);
+
+       if (complete)
+               tree->object.flags |= SEEN;
+       return complete;
+}
+
+static int commit_is_complete(struct commit *commit)
+{
+       struct object_array study;
+       struct object_array found;
+       int is_incomplete = 0;
+       int i;
+
+       /* early return */
+       if (commit->object.flags & SEEN)
+               return 1;
+       if (commit->object.flags & INCOMPLETE)
+               return 0;
+       /*
+        * Find all commits that are reachable and are not marked as
+        * SEEN.  Then make sure the trees and blobs contained are
+        * complete.  After that, mark these commits also as SEEN.
+        * If some of the objects that are needed to complete this
+        * commit are missing, mark this commit as INCOMPLETE.
+        */
+       memset(&study, 0, sizeof(study));
+       memset(&found, 0, sizeof(found));
+       add_object_array(&commit->object, NULL, &study);
+       add_object_array(&commit->object, NULL, &found);
+       commit->object.flags |= STUDYING;
+       while (study.nr) {
+               struct commit *c;
+               struct commit_list *parent;
+
+               c = (struct commit *)object_array_pop(&study);
+               if (!c->object.parsed && !parse_object(the_repository, &c->object.oid))
+                       c->object.flags |= INCOMPLETE;
+
+               if (c->object.flags & INCOMPLETE) {
+                       is_incomplete = 1;
+                       break;
+               }
+               else if (c->object.flags & SEEN)
+                       continue;
+               for (parent = c->parents; parent; parent = parent->next) {
+                       struct commit *p = parent->item;
+                       if (p->object.flags & STUDYING)
+                               continue;
+                       p->object.flags |= STUDYING;
+                       add_object_array(&p->object, NULL, &study);
+                       add_object_array(&p->object, NULL, &found);
+               }
+       }
+       if (!is_incomplete) {
+               /*
+                * make sure all commits in "found" array have all the
+                * necessary objects.
+                */
+               for (i = 0; i < found.nr; i++) {
+                       struct commit *c =
+                               (struct commit *)found.objects[i].item;
+                       if (!tree_is_complete(get_commit_tree_oid(c))) {
+                               is_incomplete = 1;
+                               c->object.flags |= INCOMPLETE;
+                       }
+               }
+               if (!is_incomplete) {
+                       /* mark all found commits as complete, iow SEEN */
+                       for (i = 0; i < found.nr; i++)
+                               found.objects[i].item->flags |= SEEN;
+               }
+       }
+       /* clear flags from the objects we traversed */
+       for (i = 0; i < found.nr; i++)
+               found.objects[i].item->flags &= ~STUDYING;
+       if (is_incomplete)
+               commit->object.flags |= INCOMPLETE;
+       else {
+               /*
+                * If we come here, we have (1) traversed the ancestry chain
+                * from the "commit" until we reach SEEN commits (which are
+                * known to be complete), and (2) made sure that the commits
+                * encountered during the above traversal refer to trees that
+                * are complete.  Which means that we know *all* the commits
+                * we have seen during this process are complete.
+                */
+               for (i = 0; i < found.nr; i++)
+                       found.objects[i].item->flags |= SEEN;
+       }
+       /* free object arrays */
+       object_array_clear(&study);
+       object_array_clear(&found);
+       return !is_incomplete;
+}
+
+static int keep_entry(struct commit **it, struct object_id *oid)
+{
+       struct commit *commit;
+
+       if (is_null_oid(oid))
+               return 1;
+       commit = lookup_commit_reference_gently(the_repository, oid, 1);
+       if (!commit)
+               return 0;
+
+       /*
+        * Make sure everything in this commit exists.
+        *
+        * We have walked all the objects reachable from the refs
+        * and cache earlier.  The commits reachable by this commit
+        * must meet SEEN commits -- and then we should mark them as
+        * SEEN as well.
+        */
+       if (!commit_is_complete(commit))
+               return 0;
+       *it = commit;
+       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_policy_cb *cb)
+{
+       struct commit_list *pending;
+       timestamp_t 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 *parent;
+               struct commit *commit = pop_commit(&pending);
+               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_policy_cb *cb, struct commit *commit, struct object_id *oid)
+{
+       /*
+        * We may or may not have the commit yet - if not, look it
+        * up using the supplied sha1.
+        */
+       if (!commit) {
+               if (is_null_oid(oid))
+                       return 0;
+
+               commit = lookup_commit_reference_gently(the_repository, oid,
+                                                       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);
+}
+
+/*
+ * Return true iff the specified reflog entry should be expired.
+ */
+int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,
+                                   const char *email, timestamp_t timestamp, int tz,
+                                   const char *message, void *cb_data)
+{
+       struct expire_reflog_policy_cb *cb = cb_data;
+       struct commit *old_commit, *new_commit;
+
+       if (timestamp < cb->cmd.expire_total)
+               return 1;
+
+       old_commit = new_commit = NULL;
+       if (cb->cmd.stalefix &&
+           (!keep_entry(&old_commit, ooid) || !keep_entry(&new_commit, noid)))
+               return 1;
+
+       if (timestamp < cb->cmd.expire_unreachable) {
+               switch (cb->unreachable_expire_kind) {
+               case UE_ALWAYS:
+                       return 1;
+               case UE_NORMAL:
+               case UE_HEAD:
+                       if (unreachable(cb, old_commit, ooid) || unreachable(cb, new_commit, noid))
+                               return 1;
+                       break;
+               }
+       }
+
+       if (cb->cmd.recno && --(cb->cmd.recno) == 0)
+               return 1;
+
+       return 0;
+}
+
+int should_expire_reflog_ent_verbose(struct object_id *ooid,
+                                           struct object_id *noid,
+                                           const char *email,
+                                           timestamp_t timestamp, int tz,
+                                           const char *message, void *cb_data)
+{
+       struct expire_reflog_policy_cb *cb = cb_data;
+       int expire;
+
+       expire = should_expire_reflog_ent(ooid, noid, email, timestamp, tz,
+                                         message, cb);
+
+       if (!expire)
+               printf("keep %s", message);
+       else if (cb->dry_run)
+               printf("would prune %s", message);
+       else
+               printf("prune %s", message);
+
+       return expire;
+}
+
+static int push_tip_to_list(const char *refname, const struct object_id *oid,
+                           int flags, void *cb_data)
+{
+       struct commit_list **list = cb_data;
+       struct commit *tip_commit;
+       if (flags & REF_ISSYMREF)
+               return 0;
+       tip_commit = lookup_commit_reference_gently(the_repository, oid, 1);
+       if (!tip_commit)
+               return 0;
+       commit_list_insert(tip_commit, list);
+       return 0;
+}
+
+static int is_head(const char *refname)
+{
+       switch (ref_type(refname)) {
+       case REF_TYPE_OTHER_PSEUDOREF:
+       case REF_TYPE_MAIN_PSEUDOREF:
+               if (parse_worktree_ref(refname, NULL, NULL, &refname))
+                       BUG("not a worktree ref: %s", refname);
+               break;
+       default:
+               break;
+       }
+       return !strcmp(refname, "HEAD");
+}
+
+void reflog_expiry_prepare(const char *refname,
+                                 const struct object_id *oid,
+                                 void *cb_data)
+{
+       struct expire_reflog_policy_cb *cb = cb_data;
+       struct commit_list *elem;
+       struct commit *commit = NULL;
+
+       if (!cb->cmd.expire_unreachable || is_head(refname)) {
+               cb->unreachable_expire_kind = UE_HEAD;
+       } else {
+               commit = lookup_commit(the_repository, oid);
+               cb->unreachable_expire_kind = commit ? UE_NORMAL : UE_ALWAYS;
+       }
+
+       if (cb->cmd.expire_unreachable <= cb->cmd.expire_total)
+               cb->unreachable_expire_kind = UE_ALWAYS;
+
+       switch (cb->unreachable_expire_kind) {
+       case UE_ALWAYS:
+               return;
+       case UE_HEAD:
+               for_each_ref(push_tip_to_list, &cb->tips);
+               for (elem = cb->tips; elem; elem = elem->next)
+                       commit_list_insert(elem->item, &cb->mark_list);
+               break;
+       case UE_NORMAL:
+               commit_list_insert(commit, &cb->mark_list);
+               /* For reflog_expiry_cleanup() below */
+               cb->tip_commit = commit;
+       }
+       cb->mark_limit = cb->cmd.expire_total;
+       mark_reachable(cb);
+}
+
+void reflog_expiry_cleanup(void *cb_data)
+{
+       struct expire_reflog_policy_cb *cb = cb_data;
+       struct commit_list *elem;
+
+       switch (cb->unreachable_expire_kind) {
+       case UE_ALWAYS:
+               return;
+       case UE_HEAD:
+               for (elem = cb->tips; elem; elem = elem->next)
+                       clear_commit_marks(elem->item, REACHABLE);
+               free_commit_list(cb->tips);
+               break;
+       case UE_NORMAL:
+               clear_commit_marks(cb->tip_commit, REACHABLE);
+               break;
+       }
+}
+
+int count_reflog_ent(struct object_id *ooid, struct object_id *noid,
+               const char *email, timestamp_t timestamp, int tz,
+               const char *message, void *cb_data)
+{
+       struct cmd_reflog_expire_cb *cb = cb_data;
+       if (!cb->expire_total || timestamp < cb->expire_total)
+               cb->recno++;
+       return 0;
+}
+
+int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose)
+{
+       struct cmd_reflog_expire_cb cmd = { 0 };
+       int status = 0;
+       reflog_expiry_should_prune_fn *should_prune_fn = should_expire_reflog_ent;
+       const char *spec = strstr(rev, "@{");
+       char *ep, *ref;
+       int recno;
+       struct expire_reflog_policy_cb cb = {
+               .dry_run = !!(flags & EXPIRE_REFLOGS_DRY_RUN),
+       };
+
+       if (verbose)
+               should_prune_fn = should_expire_reflog_ent_verbose;
+
+       if (!spec)
+               return error(_("not a reflog: %s"), rev);
+
+       if (!dwim_log(rev, spec - rev, NULL, &ref)) {
+               status |= error(_("no reflog for '%s'"), rev);
+               goto cleanup;
+       }
+
+       recno = strtoul(spec + 2, &ep, 10);
+       if (*ep == '}') {
+               cmd.recno = -recno;
+               for_each_reflog_ent(ref, count_reflog_ent, &cmd);
+       } else {
+               cmd.expire_total = approxidate(spec + 2);
+               for_each_reflog_ent(ref, count_reflog_ent, &cmd);
+               cmd.expire_total = 0;
+       }
+
+       cb.cmd = cmd;
+       status |= reflog_expire(ref, flags,
+                               reflog_expiry_prepare,
+                               should_prune_fn,
+                               reflog_expiry_cleanup,
+                               &cb);
+
+ cleanup:
+       free(ref);
+       return status;
+}
diff --git a/reflog.h b/reflog.h
new file mode 100644 (file)
index 0000000..d2906fb
--- /dev/null
+++ b/reflog.h
@@ -0,0 +1,43 @@
+#ifndef REFLOG_H
+#define REFLOG_H
+#include "refs.h"
+
+struct cmd_reflog_expire_cb {
+       int stalefix;
+       int explicit_expiry;
+       timestamp_t expire_total;
+       timestamp_t expire_unreachable;
+       int recno;
+};
+
+struct expire_reflog_policy_cb {
+       enum {
+               UE_NORMAL,
+               UE_ALWAYS,
+               UE_HEAD
+       } unreachable_expire_kind;
+       struct commit_list *mark_list;
+       unsigned long mark_limit;
+       struct cmd_reflog_expire_cb cmd;
+       struct commit *tip_commit;
+       struct commit_list *tips;
+       unsigned int dry_run:1;
+};
+
+int reflog_delete(const char *rev, enum expire_reflog_flags flags,
+                 int verbose);
+void reflog_expiry_cleanup(void *cb_data);
+void reflog_expiry_prepare(const char *refname, const struct object_id *oid,
+                          void *cb_data);
+int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid,
+                            const char *email, timestamp_t timestamp, int tz,
+                            const char *message, void *cb_data);
+int count_reflog_ent(struct object_id *ooid, struct object_id *noid,
+                    const char *email, timestamp_t timestamp, int tz,
+                    const char *message, void *cb_data);
+int should_expire_reflog_ent_verbose(struct object_id *ooid,
+                                    struct object_id *noid,
+                                    const char *email,
+                                    timestamp_t timestamp, int tz,
+                                    const char *message, void *cb_data);
+#endif /* REFLOG_H */