git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH 0/3] libify reflog
@ 2022-02-18 18:40 John Cai via GitGitGadget
  2022-02-18 18:40 ` [PATCH 1/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
                   ` (4 more replies)
  0 siblings, 5 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-18 18:40 UTC (permalink / raw)
  To: git; +Cc: John Cai

In [1], there was a discussion around a bug report of stash not recovering
in the middle of the process. It turned out to not be a bug we need to fix.
However, out of that discussion came the idea of libifying reflog. This can
stand alone as a code improvement.

stash.c currently shells out to call reflog to delete reflogs. Libify reflog
delete and call it from both builtin/reflog.c and builtin/stash.c.

This patch has three parts:

 * libify reflog's delete functionality and move some of the helpers into a
   reflog.c library and export them
 * call reflog_delete from builtin/reflog.c
 * call reflog_delete from builtin/stash.c

 1. https://lore.kernel.org/git/220126.86h79qe692.gmgdl@evledraar.gmail.com/

John Cai (3):
  reflog: libify delete reflog function and helpers
  reflog: call reflog_delete from reflog.c
  stash: call reflog_delete from reflog.c

 Makefile         |   1 +
 builtin/reflog.c | 451 +----------------------------------------------
 builtin/stash.c  |  16 +-
 reflog.c         | 432 +++++++++++++++++++++++++++++++++++++++++++++
 reflog.h         |  49 +++++
 5 files changed, 490 insertions(+), 459 deletions(-)
 create mode 100644 reflog.c
 create mode 100644 reflog.h


base-commit: b80121027d1247a0754b3cc46897fee75c050b44
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1218%2Fjohn-cai%2Fjc-libify-reflog-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1218/john-cai/jc-libify-reflog-v1
Pull-Request: https://github.com/git/git/pull/1218
-- 
gitgitgadget

^ permalink raw reply	[flat|nested] 63+ messages in thread

* [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 18:40 [PATCH 0/3] libify reflog John Cai via GitGitGadget
@ 2022-02-18 18:40 ` John Cai via GitGitGadget
  2022-02-18 19:10   ` Ævar Arnfjörð Bjarmason
                     ` (3 more replies)
  2022-02-18 18:40 ` [PATCH 2/3] reflog: call reflog_delete from reflog.c John Cai via GitGitGadget
                   ` (3 subsequent siblings)
  4 siblings, 4 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-18 18:40 UTC (permalink / raw)
  To: git; +Cc: John Cai, John Cai

From: John Cai <johncai86@gmail.com>

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>
---
 Makefile         |   1 +
 builtin/reflog.c | 409 +-------------------------------------------
 reflog.c         | 432 +++++++++++++++++++++++++++++++++++++++++++++++
 reflog.h         |  49 ++++++
 4 files changed, 484 insertions(+), 407 deletions(-)
 create mode 100644 reflog.c
 create mode 100644 reflog.h

diff --git a/Makefile b/Makefile
index 186f9ab6190..f1e295539ab 100644
--- a/Makefile
+++ b/Makefile
@@ -990,6 +990,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
diff --git a/builtin/reflog.c b/builtin/reflog.c
index 85b838720c3..65198320cd2 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -1,16 +1,13 @@
 #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 +15,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 +308,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>..."),
@@ -726,6 +320,7 @@ static int cmd_reflog_delete(int argc, const char **argv, const char *prefix)
 	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"),
diff --git a/reflog.c b/reflog.c
new file mode 100644
index 00000000000..227ed83b3da
--- /dev/null
+++ b/reflog.c
@@ -0,0 +1,432 @@
+#include "cache.h"
+#include "commit.h"
+#include "object-store.h"
+#include "reachable.h"
+#include "reflog.h"
+#include "refs.h"
+#include "revision.h"
+#include "tree-walk.h"
+#include "worktree.h"
+
+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, int 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) {
+		status |= error(_("not a reflog: %s"), rev);
+	}
+
+	if (!dwim_log(rev, spec - rev, NULL, &ref)) {
+		status |= error(_("no reflog for '%s'"), rev);
+	}
+
+	if (status)
+		return status;
+
+	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;
+}
diff --git a/reflog.h b/reflog.h
new file mode 100644
index 00000000000..e4a8a104f45
--- /dev/null
+++ b/reflog.h
@@ -0,0 +1,49 @@
+#ifndef REFLOG_H
+#define REFLOG_H
+
+#include "cache.h"
+#include "commit.h"
+
+/* Remember to update object flag allocation in object.h */
+#define INCOMPLETE	(1u<<10)
+#define STUDYING	(1u<<11)
+#define REACHABLE	(1u<<12)
+
+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*, int, int);
+void reflog_expiry_cleanup(void *);
+void reflog_expiry_prepare(const char*, const struct object_id*,
+			   void *);
+int should_expire_reflog_ent(struct object_id *, struct object_id*,
+				    const char *, timestamp_t, int,
+				    const char *, void *);
+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 *,
+				     struct object_id *,
+				     const char *,
+				     timestamp_t, int,
+				     const char *, void *);
+#endif /* REFLOG_H */
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 63+ messages in thread

* [PATCH 2/3] reflog: call reflog_delete from reflog.c
  2022-02-18 18:40 [PATCH 0/3] libify reflog John Cai via GitGitGadget
  2022-02-18 18:40 ` [PATCH 1/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
@ 2022-02-18 18:40 ` John Cai via GitGitGadget
  2022-02-18 19:15   ` Ævar Arnfjörð Bjarmason
  2022-02-18 18:40 ` [PATCH 3/3] stash: " John Cai via GitGitGadget
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-18 18:40 UTC (permalink / raw)
  To: git; +Cc: John Cai, John Cai

From: John Cai <johncai86@gmail.com>

Now that reflog is libified into reflog.c, we can call reflog_delete
from the reflog.c library.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
---
 builtin/reflog.c | 42 ++----------------------------------------
 1 file changed, 2 insertions(+), 40 deletions(-)

diff --git a/builtin/reflog.c b/builtin/reflog.c
index 65198320cd2..03d347e5832 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -316,12 +316,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),
@@ -337,48 +335,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;
-		}
-
-		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;
-		}
+	for (i = 0; i < argc; i++)
+		status |= reflog_delete(argv[i], flags, verbose);
 
-		cb.cmd = cmd;
-		status |= reflog_expire(ref, flags,
-					reflog_expiry_prepare,
-					should_prune_fn,
-					reflog_expiry_cleanup,
-					&cb);
-		free(ref);
-	}
 	return status;
 }
 
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 63+ messages in thread

* [PATCH 3/3] stash: call reflog_delete from reflog.c
  2022-02-18 18:40 [PATCH 0/3] libify reflog John Cai via GitGitGadget
  2022-02-18 18:40 ` [PATCH 1/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
  2022-02-18 18:40 ` [PATCH 2/3] reflog: call reflog_delete from reflog.c John Cai via GitGitGadget
@ 2022-02-18 18:40 ` John Cai via GitGitGadget
  2022-02-18 19:20   ` Ævar Arnfjörð Bjarmason
  2022-02-18 19:29 ` [PATCH 0/3] libify reflog Ævar Arnfjörð Bjarmason
  2022-02-22 18:30 ` [PATCH v2 " John Cai via GitGitGadget
  4 siblings, 1 reply; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-18 18:40 UTC (permalink / raw)
  To: git; +Cc: John Cai, John Cai

From: John Cai <johncai86@gmail.com>

Now that cmd_reflog_delete has been libified an exported it into a new
reflog.c library so we can call it directly from builtin/stash.c. This
not only gives us a performance gain since we don't need to create a
subprocess, but it also allows us to use the ref transactions api in the
future.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
---
 builtin/stash.c | 16 ++++------------
 1 file changed, 4 insertions(+), 12 deletions(-)

diff --git a/builtin/stash.c b/builtin/stash.c
index 9638c56303e..d0967b3d3c3 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -17,6 +17,7 @@
 #include "diffcore.h"
 #include "exec-cmd.h"
 #include "entry.h"
+#include "reflog.h"
 
 #define INCLUDE_ALL_FILES 2
 
@@ -635,18 +636,9 @@ static int reflog_is_empty(const char *refname)
 static int do_drop_stash(struct stash_info *info, int quiet)
 {
 	int ret;
-	struct child_process cp_reflog = CHILD_PROCESS_INIT;
-
-	/*
-	 * reflog does not provide a simple function for deleting refs. One will
-	 * need to be added to avoid implementing too much reflog code here
-	 */
-
-	cp_reflog.git_cmd = 1;
-	strvec_pushl(&cp_reflog.args, "reflog", "delete", "--updateref",
-		     "--rewrite", NULL);
-	strvec_push(&cp_reflog.args, info->revision.buf);
-	ret = run_command(&cp_reflog);
+	ret = reflog_delete(info->revision.buf,
+			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
+			    0);
 	if (!ret) {
 		if (!quiet)
 			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 18:40 ` [PATCH 1/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
@ 2022-02-18 19:10   ` Ævar Arnfjörð Bjarmason
  2022-02-18 19:39     ` Taylor Blau
  2022-02-18 19:35   ` Taylor Blau
                     ` (2 subsequent siblings)
  3 siblings, 1 reply; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-18 19:10 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, John Cai


On Fri, Feb 18 2022, John Cai via GitGitGadget wrote:

> From: John Cai <johncai86@gmail.com>
>
> 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>

Yay! This looks great. Even though I'll need to deal with some conflicts
locally .. :)

> +int reflog_delete(const char *rev, int 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) {
> +		status |= error(_("not a reflog: %s"), rev);
> +	}
> +
> +	if (!dwim_log(rev, spec - rev, NULL, &ref)) {
> +		status |= error(_("no reflog for '%s'"), rev);
> +	}

For these let's follow our usual style of not having braces for
single-line if's.

Buuuut in this case doing so will make the diff move detection less
useful for 1..2.

So probably best to leave it, or do some post-cleanup at the end maybe.

> +int reflog_delete(const char*, int, int);
> +void reflog_expiry_cleanup(void *);
> +void reflog_expiry_prepare(const char*, const struct object_id*,
> +			   void *);
> +int should_expire_reflog_ent(struct object_id *, struct object_id*,
> +				    const char *, timestamp_t, int,
> +				    const char *, void *);
> +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 *,
> +				     struct object_id *,
> +				     const char *,
> +				     timestamp_t, int,
> +				     const char *, void *);
> +#endif /* REFLOG_H */

Just a style preference, but I for one prefer the style where we retain
the parameter names, it helps to read these, especially when we add API
docs here.

Some of these are mis-indented. We align with the opening "(" with "\t"
= 8 chars, so e.g. 2x \t + 5 SP for the count_reflog_ent() arguments
etc.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 2/3] reflog: call reflog_delete from reflog.c
  2022-02-18 18:40 ` [PATCH 2/3] reflog: call reflog_delete from reflog.c John Cai via GitGitGadget
@ 2022-02-18 19:15   ` Ævar Arnfjörð Bjarmason
  2022-02-18 20:26     ` Junio C Hamano
  0 siblings, 1 reply; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-18 19:15 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, John Cai


On Fri, Feb 18 2022, John Cai via GitGitGadget wrote:

> From: John Cai <johncai86@gmail.com>
>
> Now that reflog is libified into reflog.c, we can call reflog_delete
> from the reflog.c library.
>
> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> Signed-off-by: John Cai <johncai86@gmail.com>
> ---
>  builtin/reflog.c | 42 ++----------------------------------------
>  1 file changed, 2 insertions(+), 40 deletions(-)
>
> diff --git a/builtin/reflog.c b/builtin/reflog.c
> index 65198320cd2..03d347e5832 100644
> --- a/builtin/reflog.c
> +++ b/builtin/reflog.c
> @@ -316,12 +316,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),
> @@ -337,48 +335,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;
> -		}
> -
> -		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;
> -		}
> +	for (i = 0; i < argc; i++)
> +		status |= reflog_delete(argv[i], flags, verbose);
>  
> -		cb.cmd = cmd;
> -		status |= reflog_expire(ref, flags,
> -					reflog_expiry_prepare,
> -					should_prune_fn,
> -					reflog_expiry_cleanup,
> -					&cb);
> -		free(ref);
> -	}
>  	return status;
>  }


Maybe others will disagree, but per my comment on 1/2 I found reviewing
this locally much easier with this squashed into 1/2 (without the {}
changes I suggested).

I.e. the diff move/rename detection eats up this change & shows that the
combinatino of 1/3 and 2/3 is almost entirely just moving around
existing code (good!).

But without this squashed 1/3 has a reflog_delete() "addition", that we
later can see is mostly just moving things around.

I'll leave it to you to decide what you want to do there, just
suggestion on an otherwise very trivial-to-review change :)

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 3/3] stash: call reflog_delete from reflog.c
  2022-02-18 18:40 ` [PATCH 3/3] stash: " John Cai via GitGitGadget
@ 2022-02-18 19:20   ` Ævar Arnfjörð Bjarmason
  2022-02-19  0:21     ` Taylor Blau
  2022-02-22  2:36     ` John Cai
  0 siblings, 2 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-18 19:20 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, John Cai


On Fri, Feb 18 2022, John Cai via GitGitGadget wrote:

> From: John Cai <johncai86@gmail.com>
>
> Now that cmd_reflog_delete has been libified an exported it into a new
> reflog.c library so we can call it directly from builtin/stash.c. This
> not only gives us a performance gain since we don't need to create a
> subprocess, but it also allows us to use the ref transactions api in the
> future.
>
> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> Signed-off-by: John Cai <johncai86@gmail.com>

Very nicely done, and nice that despite the ~500 lines added/removed in
the diffstat that the "actual" changes in this series are so small.q

> @@ -635,18 +636,9 @@ static int reflog_is_empty(const char *refname)
>  static int do_drop_stash(struct stash_info *info, int quiet)
>  {
>  	int ret;
> -	struct child_process cp_reflog = CHILD_PROCESS_INIT;
> -

Nit: We usually separate variables decls with a \n\n, as is done in the
pre-image, but you end up dropping that.

> -	/*
> -	 * reflog does not provide a simple function for deleting refs. One will
> -	 * need to be added to avoid implementing too much reflog code here
> -	 */
> -
> -	cp_reflog.git_cmd = 1;
> -	strvec_pushl(&cp_reflog.args, "reflog", "delete", "--updateref",
> -		     "--rewrite", NULL);
> -	strvec_push(&cp_reflog.args, info->revision.buf);
> -	ret = run_command(&cp_reflog);
> +	ret = reflog_delete(info->revision.buf,
> +			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
> +			    0);
>  	if (!ret) {
>  		if (!quiet)
>  			printf_ln(_("Dropped %s (%s)"), info->revision.buf,

I think per the above squashing this in would be nice, i.e. you get rid
of the long line & it'sclear that "ret" is not used for anything now:

diff --git a/builtin/stash.c b/builtin/stash.c
index d0967b3d3c3..7b939576720 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -635,11 +635,9 @@ static int reflog_is_empty(const char *refname)
 
 static int do_drop_stash(struct stash_info *info, int quiet)
 {
-	int ret;
-	ret = reflog_delete(info->revision.buf,
-			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
-			    0);
-	if (!ret) {
+	unsigned int flags = EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE;
+
+	if (!reflog_delete(info->revision.buf, flags, 0)) {
 		if (!quiet)
 			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
 				  oid_to_hex(&info->w_commit));

But, having written that I notice that we have *_REWRITE twice there, so
I almost just carried forward a new bug in 3/3 when composing this :)

So one should be EXPIRE_REFLOGS_UPDATE_REF, presumably.

And perhaps it's a big pain, but that suggests that the code isn't
either used at all, or that we're missing a test for it.

So adding a prep commit to this series where we either drop it, or add
the missing test would be a very nice addition.

See my quite recent 5ac15ad2509 (reflog tests: add --updateref tests,
2021-10-16) for adding such tests, but in that case I just covered "git
reflog" itself, not "git stash". Maybe we can just add something to that
for-loop in t1417 (or similar for a new stash test).

Also, a s/unsigned int flags/enum expire_reflog_flags/ while we're at it
would be very nice here, but that could be done as another small prep
commit. I.e. it's *not* a new issue since cmd_reflog_delete() had it
before, but when converting this to a documented API it would be very
nice to have it reflect the actual type we end up using.

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH 0/3] libify reflog
  2022-02-18 18:40 [PATCH 0/3] libify reflog John Cai via GitGitGadget
                   ` (2 preceding siblings ...)
  2022-02-18 18:40 ` [PATCH 3/3] stash: " John Cai via GitGitGadget
@ 2022-02-18 19:29 ` Ævar Arnfjörð Bjarmason
  2022-02-22 18:30 ` [PATCH v2 " John Cai via GitGitGadget
  4 siblings, 0 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-18 19:29 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, John Cai


On Fri, Feb 18 2022, John Cai via GitGitGadget wrote:

> In [1], there was a discussion around a bug report of stash not recovering
> in the middle of the process. It turned out to not be a bug we need to fix.
> However, out of that discussion came the idea of libifying reflog. This can
> stand alone as a code improvement.
>
> stash.c currently shells out to call reflog to delete reflogs. Libify reflog
> delete and call it from both builtin/reflog.c and builtin/stash.c.
>
> This patch has three parts:
>
>  * libify reflog's delete functionality and move some of the helpers into a
>    reflog.c library and export them
>  * call reflog_delete from builtin/reflog.c
>  * call reflog_delete from builtin/stash.c
>
>  1. https://lore.kernel.org/git/220126.86h79qe692.gmgdl@evledraar.gmail.com/

I just reviewed this in detail, it's very nice to see this done &
another shell-out in our code API-ified.

There's at least one bad bug here (but easily fixed), and some nits
etc. that I suggested mainly to make the diff easier to look at locally,
but aside from those minor adjustments this looks very good to me.



^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 18:40 ` [PATCH 1/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
  2022-02-18 19:10   ` Ævar Arnfjörð Bjarmason
@ 2022-02-18 19:35   ` Taylor Blau
  2022-02-21  1:43     ` John Cai
  2022-02-18 20:00   ` Junio C Hamano
  2022-02-18 20:21   ` Junio C Hamano
  3 siblings, 1 reply; 63+ messages in thread
From: Taylor Blau @ 2022-02-18 19:35 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, John Cai

On Fri, Feb 18, 2022 at 06:40:45PM +0000, John Cai via GitGitGadget wrote:
> From: John Cai <johncai86@gmail.com>
>
> 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.

Sounds great. For other reviewers, looking at this with `--color-moved`
if you have the patches applied locally makes it much easier to see what
is going on here, which is basically:

  - All of the implementation that used to be in builtin/reflog.c moved
    to reflog.c.

  - The function signatures and structure declarations moved to
    reflog.h.

> 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.

As you mentioned, the `reflog_delete()` implementation is indeed new. It
looks like Ævar reviewed most of it already, so I'll take a look at his
message before I end up repeating everything he already said ;).

It's worth noting that the subsequent clean-up to rewrite
cmd_reflog_delete() in terms of your new `reflog_delete()` happens in
the subsequent commit. If you end up rerolling this series, mentioning
that here may be worthwhile.

One question that I had which I don't see answered already is what the
plan is for existing reflog-related functions that live in refs.h.
Should or will those functions be moved to the new reflog-specific
header, too?

Thanks,
Taylor

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 19:10   ` Ævar Arnfjörð Bjarmason
@ 2022-02-18 19:39     ` Taylor Blau
  2022-02-18 19:48       ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 63+ messages in thread
From: Taylor Blau @ 2022-02-18 19:39 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: John Cai via GitGitGadget, git, John Cai

On Fri, Feb 18, 2022 at 08:10:07PM +0100, Ævar Arnfjörð Bjarmason wrote:
> > +int reflog_delete(const char *rev, int 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) {
> > +		status |= error(_("not a reflog: %s"), rev);
> > +	}
> > +
> > +	if (!dwim_log(rev, spec - rev, NULL, &ref)) {
> > +		status |= error(_("no reflog for '%s'"), rev);
> > +	}
>
> For these let's follow our usual style of not having braces for
> single-line if's.
>
> Buuuut in this case doing so will make the diff move detection less
> useful for 1..2.
>
> So probably best to leave it, or do some post-cleanup at the end maybe.

Hmm. I don't think the diff detection mechanism would have an
opportunity to kick in here, since the code is added in one patch and
then removed in another. I think I may be missing what you're trying to
say here ;).

In any case, I don't think it's a huge deal if we can't accurately
colorize this with `--color-moved`, so I'd probably just as soon clean
up the style nits in this patch.

> > +int reflog_delete(const char*, int, int);
> > +void reflog_expiry_cleanup(void *);
> > +void reflog_expiry_prepare(const char*, const struct object_id*,
> > +			   void *);
> > +int should_expire_reflog_ent(struct object_id *, struct object_id*,
> > +				    const char *, timestamp_t, int,
> > +				    const char *, void *);
> > +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 *,
> > +				     struct object_id *,
> > +				     const char *,
> > +				     timestamp_t, int,
> > +				     const char *, void *);
> > +#endif /* REFLOG_H */
>
> Just a style preference, but I for one prefer the style where we retain
> the parameter names, it helps to read these, especially when we add API
> docs here.
>
> Some of these are mis-indented. We align with the opening "(" with "\t"
> = 8 chars, so e.g. 2x \t + 5 SP for the count_reflog_ent() arguments
> etc.

Thanks for noting on both.

Thanks,
Taylor

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 19:39     ` Taylor Blau
@ 2022-02-18 19:48       ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-18 19:48 UTC (permalink / raw)
  To: Taylor Blau; +Cc: John Cai via GitGitGadget, git, John Cai


On Fri, Feb 18 2022, Taylor Blau wrote:

> On Fri, Feb 18, 2022 at 08:10:07PM +0100, Ævar Arnfjörð Bjarmason wrote:
>> > +int reflog_delete(const char *rev, int 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) {
>> > +		status |= error(_("not a reflog: %s"), rev);
>> > +	}
>> > +
>> > +	if (!dwim_log(rev, spec - rev, NULL, &ref)) {
>> > +		status |= error(_("no reflog for '%s'"), rev);
>> > +	}
>>
>> For these let's follow our usual style of not having braces for
>> single-line if's.
>>
>> Buuuut in this case doing so will make the diff move detection less
>> useful for 1..2.
>>
>> So probably best to leave it, or do some post-cleanup at the end maybe.
>
> Hmm. I don't think the diff detection mechanism would have an
> opportunity to kick in here, since the code is added in one patch and
> then removed in another. I think I may be missing what you're trying to
> say here ;).
>
> In any case, I don't think it's a huge deal if we can't accurately
> colorize this with `--color-moved`, so I'd probably just as soon clean
> up the style nits in this patch.

Yes, this should really be read in combination with my comment on 2/3 to
squash that commit into 1/3 to take full advantage of that, sorry.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 18:40 ` [PATCH 1/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
  2022-02-18 19:10   ` Ævar Arnfjörð Bjarmason
  2022-02-18 19:35   ` Taylor Blau
@ 2022-02-18 20:00   ` Junio C Hamano
  2022-02-19  2:53     ` Ævar Arnfjörð Bjarmason
  2022-02-18 20:21   ` Junio C Hamano
  3 siblings, 1 reply; 63+ messages in thread
From: Junio C Hamano @ 2022-02-18 20:00 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, John Cai

"John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:

> diff --git a/reflog.h b/reflog.h
> new file mode 100644
> index 00000000000..e4a8a104f45
> --- /dev/null
> +++ b/reflog.h
> @@ -0,0 +1,49 @@
> +#ifndef REFLOG_H
> +#define REFLOG_H
> +
> +#include "cache.h"
> +#include "commit.h"
> +
> +/* Remember to update object flag allocation in object.h */
> +#define INCOMPLETE	(1u<<10)
> +#define STUDYING	(1u<<11)
> +#define REACHABLE	(1u<<12)

These names were unique enough back when they were part of internal
implementation detail inside builtin/reflog.c but it no longer is in
the scope it is visible.  builtin/stash.c (and whatever other things
that may want to deal with reflogs in the future) is about stash
entries and wants to have a way to differentiate these symbols from
others and hint that these are about reflog operation.

Perhaps prefix them with REFLOG_ or something?

Or, do we even NEED to expose these bits outside the implementation
of reflog.c?  If these three constants are used ONLY by reflog.c and
not by builtin/reflog.c (or builtin/stash.c), then moving them to
where they are used, i.e. in reflog.c, without exposing them to
others in reflog.h, would be a far better thing to do.  That way,
they can stay with their original name, without having to add a
differentiating prefix.

I also checked if any of the following struct's can be left as a
private implementation detail, but they are shared between reflog.c
and builtin/reflog.c and need to be in reflog.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*, int, int);
> +void reflog_expiry_cleanup(void *);
> +void reflog_expiry_prepare(const char*, const struct object_id*,
> +			   void *);
> +int should_expire_reflog_ent(struct object_id *, struct object_id*,
> +				    const char *, timestamp_t, int,
> +				    const char *, void *);
> +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 *,
> +				     struct object_id *,
> +				     const char *,
> +				     timestamp_t, int,
> +				     const char *, void *);
> +#endif /* REFLOG_H */

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 18:40 ` [PATCH 1/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
                     ` (2 preceding siblings ...)
  2022-02-18 20:00   ` Junio C Hamano
@ 2022-02-18 20:21   ` Junio C Hamano
  3 siblings, 0 replies; 63+ messages in thread
From: Junio C Hamano @ 2022-02-18 20:21 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, John Cai

"John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: John Cai <johncai86@gmail.com>
>
> 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.

I do not quite get this step.  Yes, eventually cmd_reflog_delete()
and cmd_stash() can both call the new reflog_delete() function, but
at this step, cmd_reflog_delete() should ALREADY be able to call it,
but I see duplicated code.

Why?

> @@ -726,6 +320,7 @@ static int cmd_reflog_delete(int argc, const char **argv, const char *prefix)
>  	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"),

In other words, why the only change this step makes to
cmd_reflog_delete() is a style change here, instead of replacing the
logic inside its "while loop" with a call to the new helper
function, which is introduced ...

> diff --git a/reflog.c b/reflog.c
> new file mode 100644
> index 00000000000..227ed83b3da
> --- /dev/null
> +++ b/reflog.c
> ...
> +int reflog_delete(const char *rev, int flags, int verbose)
> +{

... here?

> +	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) {
> +		status |= error(_("not a reflog: %s"), rev);
> +	}

If this were "we moved one iteration of an existing loop to a
separate function, while trying to keep what the existing code
looked like as pristine as possible", then it is fine, but it does
not look like that is what is going on (and I do not think it is
feasible to do so, as the original being an interation in a loop,
it has "continue" here, instead you'd need to arrange to return an
error from here to allow the caller to work in a similar way).

So, if the patch is adding an adjusted implementation by removing
"continue;" that is not suitable in the context of this helper, it
should remove the now-unnecessary {braces} around the single-statement
block.

But I think you should keep {braces} around, because the "if spec is
NULL" case should not be a single-statement block.  You'd need to
return or this code is buggy.

> +	if (!dwim_log(rev, spec - rev, NULL, &ref)) {
> +		status |= error(_("no reflog for '%s'"), rev);
> +	}

I already hinted that the previous "if spec is NULL" code is buggy
because it does not return to allow the original caller to keep
working as before.  Because you lack an early return there, you'll
end up calling dwim_log with spec==NULL here, which does not end
well.


^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 2/3] reflog: call reflog_delete from reflog.c
  2022-02-18 19:15   ` Ævar Arnfjörð Bjarmason
@ 2022-02-18 20:26     ` Junio C Hamano
  0 siblings, 0 replies; 63+ messages in thread
From: Junio C Hamano @ 2022-02-18 20:26 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: John Cai via GitGitGadget, git, John Cai

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> Maybe others will disagree, but per my comment on 1/2 I found reviewing
> this locally much easier with this squashed into 1/2 (without the {}
> changes I suggested).

Oh, I am pretty much on the same page.

The if() block has to retain {} after all but not for the reason you
cite (i.e. help "diff --color-moved"), but for correctness reasons
it has to have some "early return to avoid using NULL" to replace
"continue", which means the body of the if() statement needs to stay
a two statement block.


^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 3/3] stash: call reflog_delete from reflog.c
  2022-02-18 19:20   ` Ævar Arnfjörð Bjarmason
@ 2022-02-19  0:21     ` Taylor Blau
  2022-02-22  2:36     ` John Cai
  1 sibling, 0 replies; 63+ messages in thread
From: Taylor Blau @ 2022-02-19  0:21 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: John Cai via GitGitGadget, git, John Cai

On Fri, Feb 18, 2022 at 08:20:13PM +0100, Ævar Arnfjörð Bjarmason wrote:
> diff --git a/builtin/stash.c b/builtin/stash.c
> index d0967b3d3c3..7b939576720 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
> @@ -635,11 +635,9 @@ static int reflog_is_empty(const char *refname)
>
>  static int do_drop_stash(struct stash_info *info, int quiet)
>  {
> -	int ret;
> -	ret = reflog_delete(info->revision.buf,
> -			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
> -			    0);
> -	if (!ret) {
> +	unsigned int flags = EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE;
> +
> +	if (!reflog_delete(info->revision.buf, flags, 0)) {
>  		if (!quiet)
>  			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
>  				  oid_to_hex(&info->w_commit));
>
> But, having written that I notice that we have *_REWRITE twice there, so
> I almost just carried forward a new bug in 3/3 when composing this :)
>
> So one should be EXPIRE_REFLOGS_UPDATE_REF, presumably.

Thanks for pointing it out. Just thinking aloud here, the old code ran:

    git reflog delete --updateref --rewrite

where `--updateref` sets the `EXPIRE_REFLOGS_UPDATE_REF` bit and
`-rewrite` sets the `EXPIRE_REFLOGS_REWRITE` bit.

So yep, you and I have the same conclusion there, and one of these flags
should be the UPDATE_REF variant.

> And perhaps it's a big pain, but that suggests that the code isn't
> either used at all, or that we're missing a test for it.

I think this is much more in the "lacking test coverage" category than
the "unused features we should remove" one.

Thanks,
Taylor

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 20:00   ` Junio C Hamano
@ 2022-02-19  2:53     ` Ævar Arnfjörð Bjarmason
  2022-02-19  3:02       ` Taylor Blau
  2022-02-20  7:49       ` Junio C Hamano
  0 siblings, 2 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-19  2:53 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: John Cai via GitGitGadget, git, John Cai


On Fri, Feb 18 2022, Junio C Hamano wrote:

> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> diff --git a/reflog.h b/reflog.h
>> new file mode 100644
>> index 00000000000..e4a8a104f45
>> --- /dev/null
>> +++ b/reflog.h
>> @@ -0,0 +1,49 @@
>> +#ifndef REFLOG_H
>> +#define REFLOG_H
>> +
>> +#include "cache.h"
>> +#include "commit.h"
>> +
>> +/* Remember to update object flag allocation in object.h */
>> +#define INCOMPLETE	(1u<<10)
>> +#define STUDYING	(1u<<11)
>> +#define REACHABLE	(1u<<12)
>
> These names were unique enough back when they were part of internal
> implementation detail inside builtin/reflog.c but it no longer is in
> the scope it is visible.  builtin/stash.c (and whatever other things
> that may want to deal with reflogs in the future) is about stash
> entries and wants to have a way to differentiate these symbols from
> others and hint that these are about reflog operation.
>
> Perhaps prefix them with REFLOG_ or something?
>
> Or, do we even NEED to expose these bits outside the implementation
> of reflog.c?  If these three constants are used ONLY by reflog.c and
> not by builtin/reflog.c (or builtin/stash.c), then moving them to
> where they are used, i.e. in reflog.c, without exposing them to
> others in reflog.h, would be a far better thing to do.  That way,
> they can stay with their original name, without having to add a
> differentiating prefix.

No objection to this, but FWIW the general pattern for these object.h
flags is to use these un-prefixed names:

     git grep -A20 'Remember to update object flag allocation' | grep define

To be fair the only one that's really comparable is the revision.h ones,
but those are very non-namespace-y, with names like SEEN, ADDED, SHOWN
etc.

I'd be fine with just leaving these as-is, especially with compilers
warning about macro re-definitions.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-19  2:53     ` Ævar Arnfjörð Bjarmason
@ 2022-02-19  3:02       ` Taylor Blau
  2022-02-20  7:49       ` Junio C Hamano
  1 sibling, 0 replies; 63+ messages in thread
From: Taylor Blau @ 2022-02-19  3:02 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: Junio C Hamano, John Cai via GitGitGadget, git, John Cai

On Sat, Feb 19, 2022 at 03:53:14AM +0100, Ævar Arnfjörð Bjarmason wrote:
>
> On Fri, Feb 18 2022, Junio C Hamano wrote:
>
> > "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
> >
> >> diff --git a/reflog.h b/reflog.h
> >> new file mode 100644
> >> index 00000000000..e4a8a104f45
> >> --- /dev/null
> >> +++ b/reflog.h
> >> @@ -0,0 +1,49 @@
> >> +#ifndef REFLOG_H
> >> +#define REFLOG_H
> >> +
> >> +#include "cache.h"
> >> +#include "commit.h"
> >> +
> >> +/* Remember to update object flag allocation in object.h */
> >> +#define INCOMPLETE	(1u<<10)
> >> +#define STUDYING	(1u<<11)
> >> +#define REACHABLE	(1u<<12)
> >
> > These names were unique enough back when they were part of internal
> > implementation detail inside builtin/reflog.c but it no longer is in
> > the scope it is visible.  builtin/stash.c (and whatever other things
> > that may want to deal with reflogs in the future) is about stash
> > entries and wants to have a way to differentiate these symbols from
> > others and hint that these are about reflog operation.
> >
> > Perhaps prefix them with REFLOG_ or something?
> >
> > Or, do we even NEED to expose these bits outside the implementation
> > of reflog.c?  If these three constants are used ONLY by reflog.c and
> > not by builtin/reflog.c (or builtin/stash.c), then moving them to
> > where they are used, i.e. in reflog.c, without exposing them to
> > others in reflog.h, would be a far better thing to do.  That way,
> > they can stay with their original name, without having to add a
> > differentiating prefix.
>
> No objection to this, but FWIW the general pattern for these object.h
> flags is to use these un-prefixed names:
>
>      git grep -A20 'Remember to update object flag allocation' | grep define
>
> To be fair the only one that's really comparable is the revision.h ones,
> but those are very non-namespace-y, with names like SEEN, ADDED, SHOWN
> etc.
>
> I'd be fine with just leaving these as-is, especially with compilers
> warning about macro re-definitions.

I think I have a mild preference to avoid polluting the set of macros
with something like INCOMPLETE outside of reflog.c's compilation unit
when possible. Though I don't really feel strongly either way.

In any case, let's make sure to update object.h's flag allocation table,
which as of this patch still indicates that these bits belong to
"builtin/reflog.c" (instead of "reflog.h" or "reflog.c", depending on
what the outcome of this discussion is).

Thanks,
Taylor

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-19  2:53     ` Ævar Arnfjörð Bjarmason
  2022-02-19  3:02       ` Taylor Blau
@ 2022-02-20  7:49       ` Junio C Hamano
  1 sibling, 0 replies; 63+ messages in thread
From: Junio C Hamano @ 2022-02-20  7:49 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: John Cai via GitGitGadget, git, John Cai

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

>> Perhaps prefix them with REFLOG_ or something?
>>
>> Or, do we even NEED to expose these bits outside the implementation
>> of reflog.c?  If these three constants are used ONLY by reflog.c and
>> not by builtin/reflog.c (or builtin/stash.c), then moving them to
>> where they are used, i.e. in reflog.c, without exposing them to
>> others in reflog.h, would be a far better thing to do.  That way,
>> they can stay with their original name, without having to add a
>> differentiating prefix.
>
> No objection to this, but FWIW the general pattern for these object.h
> flags is to use these un-prefixed names:
>
>      git grep -A20 'Remember to update object flag allocation' | grep define
>
> To be fair the only one that's really comparable is the revision.h ones,
> but those are very non-namespace-y, with names like SEEN, ADDED, SHOWN
> etc.

Yes, but I consider that revision.h owns the flag bits, and all
others that are local to a single *.c files borrow leftover bits
(and that is why the allocations can be overlapping).

And that is why my preference is to keep these within reflog.c if we
are separating it out of builtin/reflog.c to make sure they do not
pollute global namespace.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-18 19:35   ` Taylor Blau
@ 2022-02-21  1:43     ` John Cai
  2022-02-21  1:50       ` Taylor Blau
  0 siblings, 1 reply; 63+ messages in thread
From: John Cai @ 2022-02-21  1:43 UTC (permalink / raw)
  To: Taylor Blau; +Cc: John Cai via GitGitGadget, git

Hey Taylor,

On 18 Feb 2022, at 14:35, Taylor Blau wrote:

> On Fri, Feb 18, 2022 at 06:40:45PM +0000, John Cai via GitGitGadget wrote:
>> From: John Cai <johncai86@gmail.com>
>>
>> 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.
>
> Sounds great. For other reviewers, looking at this with `--color-moved`
> if you have the patches applied locally makes it much easier to see what
> is going on here, which is basically:
>
>   - All of the implementation that used to be in builtin/reflog.c moved
>     to reflog.c.
>
>   - The function signatures and structure declarations moved to
>     reflog.h.
>
>> 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.
>
> As you mentioned, the `reflog_delete()` implementation is indeed new. It
> looks like Ævar reviewed most of it already, so I'll take a look at his
> message before I end up repeating everything he already said ;).
>
> It's worth noting that the subsequent clean-up to rewrite
> cmd_reflog_delete() in terms of your new `reflog_delete()` happens in
> the subsequent commit. If you end up rerolling this series, mentioning
> that here may be worthwhile.
>
> One question that I had which I don't see answered already is what the
> plan is for existing reflog-related functions that live in refs.h.
> Should or will those functions be moved to the new reflog-specific
> header, too?

Thanks for bringing ths up! Maybe this cleanup can be included in a separate
patch series since this one is mainly about getting rid of the shell-out in
stash.c. What do you think?

>
> Thanks,
> Taylor

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-21  1:43     ` John Cai
@ 2022-02-21  1:50       ` Taylor Blau
  2022-02-23 19:50         ` John Cai
  0 siblings, 1 reply; 63+ messages in thread
From: Taylor Blau @ 2022-02-21  1:50 UTC (permalink / raw)
  To: John Cai; +Cc: John Cai via GitGitGadget, git

On Sun, Feb 20, 2022 at 08:43:14PM -0500, John Cai wrote:
> > One question that I had which I don't see answered already is what the
> > plan is for existing reflog-related functions that live in refs.h.
> > Should or will those functions be moved to the new reflog-specific
> > header, too?
>
> Thanks for bringing ths up! Maybe this cleanup can be included in a separate
> patch series since this one is mainly about getting rid of the shell-out in
> stash.c. What do you think?

Yeah; I think that's fine. We don't need to get all reflog-related
functions moved into reflog.h in the first series, so I think it's fine
to move things as you discover them.

I was just wondering whether it was something you planned to do at some
point (as part of this series, or a future one).

Thanks,
Taylor

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 3/3] stash: call reflog_delete from reflog.c
  2022-02-18 19:20   ` Ævar Arnfjörð Bjarmason
  2022-02-19  0:21     ` Taylor Blau
@ 2022-02-22  2:36     ` John Cai
  2022-02-22 10:51       ` Ævar Arnfjörð Bjarmason
  1 sibling, 1 reply; 63+ messages in thread
From: John Cai @ 2022-02-22  2:36 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason; +Cc: John Cai via GitGitGadget, git

Hi Ævar,

On 18 Feb 2022, at 14:20, Ævar Arnfjörð Bjarmason wrote:

> On Fri, Feb 18 2022, John Cai via GitGitGadget wrote:
>
>> From: John Cai <johncai86@gmail.com>
>>
>> Now that cmd_reflog_delete has been libified an exported it into a new
>> reflog.c library so we can call it directly from builtin/stash.c. This
>> not only gives us a performance gain since we don't need to create a
>> subprocess, but it also allows us to use the ref transactions api in the
>> future.
>>
>> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
>> Signed-off-by: John Cai <johncai86@gmail.com>
>
> Very nicely done, and nice that despite the ~500 lines added/removed in
> the diffstat that the "actual" changes in this series are so small.q
>
>> @@ -635,18 +636,9 @@ static int reflog_is_empty(const char *refname)
>>  static int do_drop_stash(struct stash_info *info, int quiet)
>>  {
>>  	int ret;
>> -	struct child_process cp_reflog = CHILD_PROCESS_INIT;
>> -
>
> Nit: We usually separate variables decls with a \n\n, as is done in the
> pre-image, but you end up dropping that.
>
>> -	/*
>> -	 * reflog does not provide a simple function for deleting refs. One will
>> -	 * need to be added to avoid implementing too much reflog code here
>> -	 */
>> -
>> -	cp_reflog.git_cmd = 1;
>> -	strvec_pushl(&cp_reflog.args, "reflog", "delete", "--updateref",
>> -		     "--rewrite", NULL);
>> -	strvec_push(&cp_reflog.args, info->revision.buf);
>> -	ret = run_command(&cp_reflog);
>> +	ret = reflog_delete(info->revision.buf,
>> +			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
>> +			    0);
>>  	if (!ret) {
>>  		if (!quiet)
>>  			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
>
> I think per the above squashing this in would be nice, i.e. you get rid
> of the long line & it'sclear that "ret" is not used for anything now:
>
> diff --git a/builtin/stash.c b/builtin/stash.c
> index d0967b3d3c3..7b939576720 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
> @@ -635,11 +635,9 @@ static int reflog_is_empty(const char *refname)
>
>  static int do_drop_stash(struct stash_info *info, int quiet)
>  {
> -	int ret;
> -	ret = reflog_delete(info->revision.buf,
> -			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
> -			    0);
> -	if (!ret) {
> +	unsigned int flags = EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE;
> +
> +	if (!reflog_delete(info->revision.buf, flags, 0)) {
>  		if (!quiet)
>  			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
>  				  oid_to_hex(&info->w_commit));
>
> But, having written that I notice that we have *_REWRITE twice there, so
> I almost just carried forward a new bug in 3/3 when composing this :)
>
> So one should be EXPIRE_REFLOGS_UPDATE_REF, presumably.
>
> And perhaps it's a big pain, but that suggests that the code isn't
> either used at all, or that we're missing a test for it.
>
> So adding a prep commit to this series where we either drop it, or add
> the missing test would be a very nice addition.
>
> See my quite recent 5ac15ad2509 (reflog tests: add --updateref tests,
> 2021-10-16) for adding such tests, but in that case I just covered "git
> reflog" itself, not "git stash". Maybe we can just add something to that
> for-loop in t1417 (or similar for a new stash test).

So I was trying to write a test to exercise the reflog --updateref and --rewrite
cases. --updateref is pretty straightforward, but with --rewrite I noticed that
it rewrites the old sha in the .git/log/refs/stash file. But, I was having
trouble finding somewhere that read this value. The test could reach into this file
and check the literal contents, but wondering if there is a better way. Any help appreciated!

>
> Also, a s/unsigned int flags/enum expire_reflog_flags/ while we're at it
> would be very nice here, but that could be done as another small prep
> commit. I.e. it's *not* a new issue since cmd_reflog_delete() had it
> before, but when converting this to a documented API it would be very
> nice to have it reflect the actual type we end up using.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 3/3] stash: call reflog_delete from reflog.c
  2022-02-22  2:36     ` John Cai
@ 2022-02-22 10:51       ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-22 10:51 UTC (permalink / raw)
  To: John Cai; +Cc: John Cai via GitGitGadget, git


On Mon, Feb 21 2022, John Cai wrote:

> Hi Ævar,
>
> On 18 Feb 2022, at 14:20, Ævar Arnfjörð Bjarmason wrote:
>
>> On Fri, Feb 18 2022, John Cai via GitGitGadget wrote:
>>
>>> From: John Cai <johncai86@gmail.com>
>>>
>>> Now that cmd_reflog_delete has been libified an exported it into a new
>>> reflog.c library so we can call it directly from builtin/stash.c. This
>>> not only gives us a performance gain since we don't need to create a
>>> subprocess, but it also allows us to use the ref transactions api in the
>>> future.
>>>
>>> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
>>> Signed-off-by: John Cai <johncai86@gmail.com>
>>
>> Very nicely done, and nice that despite the ~500 lines added/removed in
>> the diffstat that the "actual" changes in this series are so small.q
>>
>>> @@ -635,18 +636,9 @@ static int reflog_is_empty(const char *refname)
>>>  static int do_drop_stash(struct stash_info *info, int quiet)
>>>  {
>>>  	int ret;
>>> -	struct child_process cp_reflog = CHILD_PROCESS_INIT;
>>> -
>>
>> Nit: We usually separate variables decls with a \n\n, as is done in the
>> pre-image, but you end up dropping that.
>>
>>> -	/*
>>> -	 * reflog does not provide a simple function for deleting refs. One will
>>> -	 * need to be added to avoid implementing too much reflog code here
>>> -	 */
>>> -
>>> -	cp_reflog.git_cmd = 1;
>>> -	strvec_pushl(&cp_reflog.args, "reflog", "delete", "--updateref",
>>> -		     "--rewrite", NULL);
>>> -	strvec_push(&cp_reflog.args, info->revision.buf);
>>> -	ret = run_command(&cp_reflog);
>>> +	ret = reflog_delete(info->revision.buf,
>>> +			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
>>> +			    0);
>>>  	if (!ret) {
>>>  		if (!quiet)
>>>  			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
>>
>> I think per the above squashing this in would be nice, i.e. you get rid
>> of the long line & it'sclear that "ret" is not used for anything now:
>>
>> diff --git a/builtin/stash.c b/builtin/stash.c
>> index d0967b3d3c3..7b939576720 100644
>> --- a/builtin/stash.c
>> +++ b/builtin/stash.c
>> @@ -635,11 +635,9 @@ static int reflog_is_empty(const char *refname)
>>
>>  static int do_drop_stash(struct stash_info *info, int quiet)
>>  {
>> -	int ret;
>> -	ret = reflog_delete(info->revision.buf,
>> -			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
>> -			    0);
>> -	if (!ret) {
>> +	unsigned int flags = EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE;
>> +
>> +	if (!reflog_delete(info->revision.buf, flags, 0)) {
>>  		if (!quiet)
>>  			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
>>  				  oid_to_hex(&info->w_commit));
>>
>> But, having written that I notice that we have *_REWRITE twice there, so
>> I almost just carried forward a new bug in 3/3 when composing this :)
>>
>> So one should be EXPIRE_REFLOGS_UPDATE_REF, presumably.
>>
>> And perhaps it's a big pain, but that suggests that the code isn't
>> either used at all, or that we're missing a test for it.
>>
>> So adding a prep commit to this series where we either drop it, or add
>> the missing test would be a very nice addition.
>>
>> See my quite recent 5ac15ad2509 (reflog tests: add --updateref tests,
>> 2021-10-16) for adding such tests, but in that case I just covered "git
>> reflog" itself, not "git stash". Maybe we can just add something to that
>> for-loop in t1417 (or similar for a new stash test).
>
> So I was trying to write a test to exercise the reflog --updateref and --rewrite
> cases. --updateref is pretty straightforward, but with --rewrite I noticed that
> it rewrites the old sha in the .git/log/refs/stash file. But, I was having
> trouble finding somewhere that read this value. The test could reach into this file
> and check the literal contents, but wondering if there is a better way. Any help appreciated!

I can check it out, but to make that easier can you share the WIP diff
you have for getting the test setup that far?

I think you mean that it munges the SHA-1 on the LHS of
.git/refs/logs/stash, I tried to do that now locally and I didn't come
up with some way where you could observably make "git stash show", "git
stash list" etc. show anything different.

I.e. it'll just promiscuously show whatever OID is on the RHS, even in
cases where some of them are turned into "deadbeef..." (i.e. "list", but
"show" will die).

So maybe it's just observable with a subsequent "git fsck", but I just
tried composing a reflog with these entries:

    0000000000000000000000000000000000000000 A
    A B
    B C

And then manually changing it to:

    0000000000000000000000000000000000000000 A
    A B
    A C

Which "git fsck" will pass, and only complain if that "A" isn't a valid
OID at all.

Anyway, if there isn't a way to get fsck/reflog to spew it out, but we
want to assert that it's correct wouldn't this catch it (will need to
depend on REFFILES) (untested):

	cut -d' ' -f1-2 .git/logs/refs/stash >actual &&
	cat >expect <<-EOF &&
	$(test_oid zero) $(git rev-parse ...)
	$(git rev-parse ...) $(git rev-parse ...)
	EOF
	test_cmp expect actual

I.e. to simply run whatever operation we do now, check that the OIDs
match what we expect, and which would be different if one of these flags
wasn't given?

>>
>> Also, a s/unsigned int flags/enum expire_reflog_flags/ while we're at it
>> would be very nice here, but that could be done as another small prep
>> commit. I.e. it's *not* a new issue since cmd_reflog_delete() had it
>> before, but when converting this to a documented API it would be very
>> nice to have it reflect the actual type we end up using.


^ permalink raw reply	[flat|nested] 63+ messages in thread

* [PATCH v2 0/3] libify reflog
  2022-02-18 18:40 [PATCH 0/3] libify reflog John Cai via GitGitGadget
                   ` (3 preceding siblings ...)
  2022-02-18 19:29 ` [PATCH 0/3] libify reflog Ævar Arnfjörð Bjarmason
@ 2022-02-22 18:30 ` John Cai via GitGitGadget
  2022-02-22 18:30   ` [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
                     ` (3 more replies)
  4 siblings, 4 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-22 18:30 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai

In [1], there was a discussion around a bug report of stash not recovering
in the middle of the process when killed with ctl-c. It turned out to not be
a bug we need to fix. However, out of that discussion came the idea of
libifying reflog. This can stand alone as a code improvement.

stash.c currently shells out to call reflog to delete reflogs. Libify reflog
delete and call it from both builtin/reflog.c and builtin/stash.c.

This patch has three parts:

 * add missing test coverage for git stash delete
 * libify reflog's delete functionality and move some of the helpers into a
   reflog.c library and call reflog_delete from builtin/reflog.c
 * call reflog_delete from builtin/stash.c

Updates since v1:

 * added missing test coverage
 * squashed 1/3 and 2/3 together
 * moved enum into reflog.c
 * updated object.h's flag allocation mapping

 1. https://lore.kernel.org/git/220126.86h79qe692.gmgdl@evledraar.gmail.com/

John Cai (3):
  stash: add test to ensure reflog --rewrite --updatref behavior
  reflog: libify delete reflog function and helpers
  stash: call reflog_delete() in reflog.c

 Makefile         |   1 +
 builtin/reflog.c | 451 +----------------------------------------------
 builtin/stash.c  |  18 +-
 object.h         |   2 +-
 reflog.c         | 435 +++++++++++++++++++++++++++++++++++++++++++++
 reflog.h         |  49 +++++
 t/t3903-stash.sh |  25 ++-
 7 files changed, 518 insertions(+), 463 deletions(-)
 create mode 100644 reflog.c
 create mode 100644 reflog.h


base-commit: e6ebfd0e8cbbd10878070c8a356b5ad1b3ca464e
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1218%2Fjohn-cai%2Fjc-libify-reflog-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1218/john-cai/jc-libify-reflog-v2
Pull-Request: https://github.com/git/git/pull/1218

Range-diff vs v1:

 -:  ----------- > 1:  6e136b62ca4 stash: add test to ensure reflog --rewrite --updatref behavior
 1:  9e17ece8d89 ! 2:  e7c950218b1 reflog: libify delete reflog function and helpers
     @@ builtin/reflog.c: static int cmd_reflog_expire(int argc, const char **argv, cons
       static const char * reflog_delete_usage[] = {
       	N_("git reflog delete [--rewrite] [--updateref] "
       	   "[--dry-run | -n] [--verbose] <refs>..."),
     -@@ builtin/reflog.c: static int cmd_reflog_delete(int argc, const char **argv, const char *prefix)
     +@@ builtin/reflog.c: 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;
      +
     - 	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),
     +@@ builtin/reflog.c: 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;
     + }
     + 
     +
     + ## object.h ##
     +@@ object.h: 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
     +  */
      
       ## reflog.c (new) ##
      @@
     @@ reflog.c (new)
      +#include "tree-walk.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;
     @@ reflog.c (new)
      +	return 0;
      +}
      +
     -+int reflog_delete(const char *rev, int flags, int verbose)
     ++int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose)
      +{
      +	struct cmd_reflog_expire_cb cmd = { 0 };
      +	int status = 0;
     @@ reflog.c (new)
      +	if (verbose)
      +		should_prune_fn = should_expire_reflog_ent_verbose;
      +
     -+	if (!spec) {
     -+		status |= error(_("not a reflog: %s"), rev);
     -+	}
     ++	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;
      +	}
      +
     -+	if (status)
     -+		return status;
     -+
      +	recno = strtoul(spec + 2, &ep, 10);
      +	if (*ep == '}') {
      +		cmd.recno = -recno;
     @@ reflog.c (new)
      +				should_prune_fn,
      +				reflog_expiry_cleanup,
      +				&cb);
     -+	free(ref);
      +
     ++ cleanup:
     ++	free(ref);
      +	return status;
      +}
      
     @@ reflog.h (new)
      +#ifndef REFLOG_H
      +#define REFLOG_H
      +
     -+#include "cache.h"
     -+#include "commit.h"
     -+
     -+/* Remember to update object flag allocation in object.h */
     -+#define INCOMPLETE	(1u<<10)
     -+#define STUDYING	(1u<<11)
     -+#define REACHABLE	(1u<<12)
     ++#include "refs.h"
      +
      +struct cmd_reflog_expire_cb {
      +	int stalefix;
     @@ reflog.h (new)
      +	unsigned int dry_run:1;
      +};
      +
     -+int reflog_delete(const char*, int, int);
     -+void reflog_expiry_cleanup(void *);
     -+void reflog_expiry_prepare(const char*, const struct object_id*,
     -+			   void *);
     -+int should_expire_reflog_ent(struct object_id *, struct object_id*,
     -+				    const char *, timestamp_t, int,
     -+				    const char *, void *);
     ++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 *,
     -+				     struct object_id *,
     -+				     const char *,
     -+				     timestamp_t, int,
     -+				     const char *, void *);
     ++		     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 */
 2:  e4c0047a17c < -:  ----------- reflog: call reflog_delete from reflog.c
 3:  bcc1eae0531 ! 3:  a023a70092b stash: call reflog_delete from reflog.c
     @@ Metadata
      Author: John Cai <johncai86@gmail.com>
      
       ## Commit message ##
     -    stash: call reflog_delete from reflog.c
     +    stash: call reflog_delete() in reflog.c
      
          Now that cmd_reflog_delete has been libified an exported it into a new
          reflog.c library so we can call it directly from builtin/stash.c. This
     @@ builtin/stash.c
       #define INCLUDE_ALL_FILES 2
       
      @@ builtin/stash.c: static int reflog_is_empty(const char *refname)
     + 
       static int do_drop_stash(struct stash_info *info, int quiet)
       {
     - 	int ret;
     +-	int ret;
      -	struct child_process cp_reflog = CHILD_PROCESS_INIT;
      -
      -	/*
     @@ builtin/stash.c: static int reflog_is_empty(const char *refname)
      -		     "--rewrite", NULL);
      -	strvec_push(&cp_reflog.args, info->revision.buf);
      -	ret = run_command(&cp_reflog);
     -+	ret = reflog_delete(info->revision.buf,
     -+			    EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_REWRITE,
     -+			    0);
     - 	if (!ret) {
     +-	if (!ret) {
     ++	if (!reflog_delete(info->revision.buf,
     ++			   EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,
     ++			   0)) {
       		if (!quiet)
       			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
     + 				  oid_to_hex(&info->w_commit));

-- 
gitgitgadget

^ permalink raw reply	[flat|nested] 63+ messages in thread

* [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-22 18:30 ` [PATCH v2 " John Cai via GitGitGadget
@ 2022-02-22 18:30   ` John Cai via GitGitGadget
  2022-02-23  8:54     ` Ævar Arnfjörð Bjarmason
  2022-02-22 18:30   ` [PATCH v2 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
                     ` (2 subsequent siblings)
  3 siblings, 1 reply; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-22 18:30 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

There is missing test coverage to ensure that the resulting reflogs
after a git stash drop has had its old oid rewritten if applicable, and
if the refs/stash has been updated if applicable.

Add two tests that verify both of these happen.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
---
 t/t3903-stash.sh | 25 ++++++++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)

diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index b149e2af441..ec9cc5646d6 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -185,10 +185,33 @@ test_expect_success 'drop middle stash by index' '
 	test 1 = $(git show HEAD:file)
 '
 
+test_expect_success 'drop stash reflog updates refs/stash' '
+	git reset --hard &&
+	git rev-parse refs/stash >expect &&
+	echo 9 >file &&
+	git stash &&
+	git stash drop stash@{0} &&
+	git rev-parse refs/stash >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
+	git reset --hard &&
+	echo 9 >file &&
+	git stash &&
+	oid="$(git rev-parse stash@{0})" &&
+	git stash drop stash@{1} &&
+	cut -d" " -f1-2 .git/logs/refs/stash >actual &&
+	cat >expect <<-EOF &&
+	$(test_oid zero) $oid
+	EOF
+	test_cmp expect actual
+'
+
 test_expect_success 'stash pop' '
 	git reset --hard &&
 	git stash pop &&
-	test 3 = $(cat file) &&
+	test 9 = $(cat file) &&
 	test 1 = $(git show :file) &&
 	test 1 = $(git show HEAD:file) &&
 	test 0 = $(git stash list | wc -l)
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 63+ messages in thread

* [PATCH v2 2/3] reflog: libify delete reflog function and helpers
  2022-02-22 18:30 ` [PATCH v2 " John Cai via GitGitGadget
  2022-02-22 18:30   ` [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
@ 2022-02-22 18:30   ` John Cai via GitGitGadget
  2022-02-23  9:02     ` Ævar Arnfjörð Bjarmason
  2022-02-23 21:28     ` Junio C Hamano
  2022-02-22 18:30   ` [PATCH v2 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
  2022-02-25 19:30   ` [PATCH v3 0/3] libify reflog John Cai via GitGitGadget
  3 siblings, 2 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-22 18:30 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

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>
---
 Makefile         |   1 +
 builtin/reflog.c | 451 +----------------------------------------------
 object.h         |   2 +-
 reflog.c         | 435 +++++++++++++++++++++++++++++++++++++++++++++
 reflog.h         |  49 +++++
 5 files changed, 490 insertions(+), 448 deletions(-)
 create mode 100644 reflog.c
 create mode 100644 reflog.h

diff --git a/Makefile b/Makefile
index 6f0b4b775fe..876d4dfd6cb 100644
--- 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
diff --git a/builtin/reflog.c b/builtin/reflog.c
index 85b838720c3..03d347e5832 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -1,16 +1,13 @@
 #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 +15,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 +308,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 +316,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 +335,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;
 }
 
diff --git a/object.h b/object.h
index cb556ab7753..a2219464c2b 100644
--- 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
index 00000000000..8d57dc43503
--- /dev/null
+++ b/reflog.c
@@ -0,0 +1,435 @@
+#include "cache.h"
+#include "commit.h"
+#include "object-store.h"
+#include "reachable.h"
+#include "reflog.h"
+#include "refs.h"
+#include "revision.h"
+#include "tree-walk.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
index 00000000000..3427021cdc2
--- /dev/null
+++ b/reflog.h
@@ -0,0 +1,49 @@
+#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 */
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 63+ messages in thread

* [PATCH v2 3/3] stash: call reflog_delete() in reflog.c
  2022-02-22 18:30 ` [PATCH v2 " John Cai via GitGitGadget
  2022-02-22 18:30   ` [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
  2022-02-22 18:30   ` [PATCH v2 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
@ 2022-02-22 18:30   ` John Cai via GitGitGadget
  2022-02-25 19:30   ` [PATCH v3 0/3] libify reflog John Cai via GitGitGadget
  3 siblings, 0 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-22 18:30 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

Now that cmd_reflog_delete has been libified an exported it into a new
reflog.c library so we can call it directly from builtin/stash.c. This
not only gives us a performance gain since we don't need to create a
subprocess, but it also allows us to use the ref transactions api in the
future.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
---
 builtin/stash.c | 18 ++++--------------
 1 file changed, 4 insertions(+), 14 deletions(-)

diff --git a/builtin/stash.c b/builtin/stash.c
index 5897febfbec..3e2f478b761 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -17,6 +17,7 @@
 #include "diffcore.h"
 #include "exec-cmd.h"
 #include "entry.h"
+#include "reflog.h"
 
 #define INCLUDE_ALL_FILES 2
 
@@ -634,20 +635,9 @@ static int reflog_is_empty(const char *refname)
 
 static int do_drop_stash(struct stash_info *info, int quiet)
 {
-	int ret;
-	struct child_process cp_reflog = CHILD_PROCESS_INIT;
-
-	/*
-	 * reflog does not provide a simple function for deleting refs. One will
-	 * need to be added to avoid implementing too much reflog code here
-	 */
-
-	cp_reflog.git_cmd = 1;
-	strvec_pushl(&cp_reflog.args, "reflog", "delete", "--updateref",
-		     "--rewrite", NULL);
-	strvec_push(&cp_reflog.args, info->revision.buf);
-	ret = run_command(&cp_reflog);
-	if (!ret) {
+	if (!reflog_delete(info->revision.buf,
+			   EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,
+			   0)) {
 		if (!quiet)
 			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
 				  oid_to_hex(&info->w_commit));
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-22 18:30   ` [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
@ 2022-02-23  8:54     ` Ævar Arnfjörð Bjarmason
  2022-02-23 21:27       ` Junio C Hamano
  2022-02-23 22:51       ` Junio C Hamano
  0 siblings, 2 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-23  8:54 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, Taylor Blau, John Cai


On Tue, Feb 22 2022, John Cai via GitGitGadget wrote:

> From: John Cai <johncai86@gmail.com>
>
> There is missing test coverage to ensure that the resulting reflogs
> after a git stash drop has had its old oid rewritten if applicable, and
> if the refs/stash has been updated if applicable.

This test looks good, and if 3/3 is applied and either of the flags
you're passing is omitted they'll fail, so we know we have the missing
coverage here.

> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> Signed-off-by: John Cai <johncai86@gmail.com>
> ---
>  t/t3903-stash.sh | 25 ++++++++++++++++++++++++-
>  1 file changed, 24 insertions(+), 1 deletion(-)
>
> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> index b149e2af441..ec9cc5646d6 100755
> --- a/t/t3903-stash.sh
> +++ b/t/t3903-stash.sh
> @@ -185,10 +185,33 @@ test_expect_success 'drop middle stash by index' '
>  	test 1 = $(git show HEAD:file)
>  '
>  
> +test_expect_success 'drop stash reflog updates refs/stash' '
> +	git reset --hard &&
> +	git rev-parse refs/stash >expect &&
> +	echo 9 >file &&
> +	git stash &&
> +	git stash drop stash@{0} &&
> +	git rev-parse refs/stash >actual &&
> +	test_cmp expect actual
> +'

This one will be portable to the reftable backend.

> +test_expect_success 'drop stash reflog updates refs/stash with rewrite' '

But as I noted in <220222.86fsob88h7.gmgdl@evledraar.gmail.com> (but it
was easy to miss) this test will need to depend on REFFILES. So just
changing this line to:

    test_expect_success REFFILES 'drop stash[...]'

> +	git reset --hard &&
> +	echo 9 >file &&
> +	git stash &&
> +	oid="$(git rev-parse stash@{0})" &&
> +	git stash drop stash@{1} &&
> +	cut -d" " -f1-2 .git/logs/refs/stash >actual &&
> +	cat >expect <<-EOF &&
> +	$(test_oid zero) $oid
> +	EOF
> +	test_cmp expect actual
> +'

Then:

>  test_expect_success 'stash pop' '
>  	git reset --hard &&
>  	git stash pop &&
> -	test 3 = $(cat file) &&
> +	test 9 = $(cat file) &&
>  	test 1 = $(git show :file) &&
>  	test 1 = $(git show HEAD:file) &&
>  	test 0 = $(git stash list | wc -l)

This test was already a bit broken in needing the preceding tests, but
it will break now if REFFILES isn't true, which you can reproduce
e.g. with:

    ./t3903-stash.sh --run=1-16,18-50 -vixd

Perhaps the least sucky solution to that is:

diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index ec9cc5646d6..1d11c9bda20 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -205,13 +205,19 @@ test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
 	cat >expect <<-EOF &&
 	$(test_oid zero) $oid
 	EOF
-	test_cmp expect actual
+	test_cmp expect actual &&
+	>dropped-stash
 '
 
 test_expect_success 'stash pop' '
 	git reset --hard &&
 	git stash pop &&
-	test 9 = $(cat file) &&
+	if test -e dropped-stash
+	then
+		test 9 = $(cat file)
+	else
+		test 3 = $(cat file)
+	fi &&
 	test 1 = $(git show :file) &&
 	test 1 = $(git show HEAD:file) &&
 	test 0 = $(git stash list | wc -l)

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 2/3] reflog: libify delete reflog function and helpers
  2022-02-22 18:30   ` [PATCH v2 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
@ 2022-02-23  9:02     ` Ævar Arnfjörð Bjarmason
  2022-02-23 18:40       ` John Cai
  2022-02-23 21:28     ` Junio C Hamano
  1 sibling, 1 reply; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-23  9:02 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, Taylor Blau, John Cai


On Tue, Feb 22 2022, John Cai via GitGitGadget wrote:

> From: John Cai <johncai86@gmail.com>
>
> 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>
> ---
>  Makefile         |   1 +
>  builtin/reflog.c | 451 +----------------------------------------------
>  object.h         |   2 +-
>  reflog.c         | 435 +++++++++++++++++++++++++++++++++++++++++++++
>  reflog.h         |  49 +++++
>  5 files changed, 490 insertions(+), 448 deletions(-)
>  create mode 100644 reflog.c
>  create mode 100644 reflog.h
>
> diff --git a/Makefile b/Makefile
> index 6f0b4b775fe..876d4dfd6cb 100644
> --- 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
> diff --git a/builtin/reflog.c b/builtin/reflog.c
> index 85b838720c3..03d347e5832 100644
> --- a/builtin/reflog.c
> +++ b/builtin/reflog.c
> @@ -1,16 +1,13 @@
>  #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"
> [...]
> diff --git a/reflog.c b/reflog.c
> new file mode 100644
> index 00000000000..8d57dc43503
> --- /dev/null
> +++ b/reflog.c
> @@ -0,0 +1,435 @@
> +#include "cache.h"
> +#include "commit.h"
> +#include "object-store.h"
> +#include "reachable.h"
> +#include "reflog.h"
> +#include "refs.h"
> +#include "revision.h"
> +#include "tree-walk.h"
> +#include "worktree.h"

I think you missed some now-redundant headers, and copied over others we
didn't need. This compiles for me with this on top:

diff --git a/builtin/reflog.c b/builtin/reflog.c
index 03d347e5832..940db196f62 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -1,9 +1,5 @@
 #include "builtin.h"
 #include "config.h"
-#include "lockfile.h"
-#include "repository.h"
-#include "dir.h"
-#include "diff.h"
 #include "revision.h"
 #include "reachable.h"
 #include "worktree.h"
diff --git a/reflog.c b/reflog.c
index 8d57dc43503..333fd8708fe 100644
--- a/reflog.c
+++ b/reflog.c
@@ -1,11 +1,8 @@
 #include "cache.h"
-#include "commit.h"
 #include "object-store.h"
-#include "reachable.h"
 #include "reflog.h"
 #include "refs.h"
 #include "revision.h"
-#include "tree-walk.h"
 #include "worktree.h"

But perhaps some of those are really "needed" but brought in implicitly?

> [...]
> diff --git a/reflog.h b/reflog.h
> new file mode 100644
> index 00000000000..3427021cdc2
> --- /dev/null
> +++ b/reflog.h
> @@ -0,0 +1,49 @@
> +#ifndef REFLOG_H
> +#define REFLOG_H
> +
> +#include "refs.h"

Just a nit but I think the reflog_delete() should be wrapped (ends up at
80 cols), and the usual style in this project is to not whitespace-pad
so much, i.e. this on top:

diff --git a/reflog.h b/reflog.h
index 3427021cdc2..d2906fb9f8d 100644
--- a/reflog.h
+++ b/reflog.h
@@ -1,6 +1,5 @@
 #ifndef REFLOG_H
 #define REFLOG_H
-
 #include "refs.h"
 
 struct cmd_reflog_expire_cb {
@@ -25,25 +24,20 @@ struct expire_reflog_policy_cb {
 	unsigned int dry_run:1;
 };
 
-int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose);
-
+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 */


Other than all that I really can't find anything at all to comment on,
and I see that all the points raised in previous rounds by others were
addressed.

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 2/3] reflog: libify delete reflog function and helpers
  2022-02-23  9:02     ` Ævar Arnfjörð Bjarmason
@ 2022-02-23 18:40       ` John Cai
  0 siblings, 0 replies; 63+ messages in thread
From: John Cai @ 2022-02-23 18:40 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: John Cai via GitGitGadget, git, Taylor Blau

Hi Ævar,

Thanks for the review!

On 23 Feb 2022, at 4:02, Ævar Arnfjörð Bjarmason wrote:

> On Tue, Feb 22 2022, John Cai via GitGitGadget wrote:
>
>> From: John Cai <johncai86@gmail.com>
>>
>> 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>
>> ---
>>  Makefile         |   1 +
>>  builtin/reflog.c | 451 +----------------------------------------------
>>  object.h         |   2 +-
>>  reflog.c         | 435 +++++++++++++++++++++++++++++++++++++++++++++
>>  reflog.h         |  49 +++++
>>  5 files changed, 490 insertions(+), 448 deletions(-)
>>  create mode 100644 reflog.c
>>  create mode 100644 reflog.h
>>
>> diff --git a/Makefile b/Makefile
>> index 6f0b4b775fe..876d4dfd6cb 100644
>> --- 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
>> diff --git a/builtin/reflog.c b/builtin/reflog.c
>> index 85b838720c3..03d347e5832 100644
>> --- a/builtin/reflog.c
>> +++ b/builtin/reflog.c
>> @@ -1,16 +1,13 @@
>>  #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"
>> [...]
>> diff --git a/reflog.c b/reflog.c
>> new file mode 100644
>> index 00000000000..8d57dc43503
>> --- /dev/null
>> +++ b/reflog.c
>> @@ -0,0 +1,435 @@
>> +#include "cache.h"
>> +#include "commit.h"
>> +#include "object-store.h"
>> +#include "reachable.h"
>> +#include "reflog.h"
>> +#include "refs.h"
>> +#include "revision.h"
>> +#include "tree-walk.h"
>> +#include "worktree.h"
>
> I think you missed some now-redundant headers, and copied over others we
> didn't need. This compiles for me with this on top:

Ah yeah, looks I left these in by mistake. Thanks for catching this.

>
> diff --git a/builtin/reflog.c b/builtin/reflog.c
> index 03d347e5832..940db196f62 100644
> --- a/builtin/reflog.c
> +++ b/builtin/reflog.c
> @@ -1,9 +1,5 @@
>  #include "builtin.h"
>  #include "config.h"
> -#include "lockfile.h"
> -#include "repository.h"
> -#include "dir.h"
> -#include "diff.h"
>  #include "revision.h"
>  #include "reachable.h"
>  #include "worktree.h"
> diff --git a/reflog.c b/reflog.c
> index 8d57dc43503..333fd8708fe 100644
> --- a/reflog.c
> +++ b/reflog.c
> @@ -1,11 +1,8 @@
>  #include "cache.h"
> -#include "commit.h"
>  #include "object-store.h"
> -#include "reachable.h"
>  #include "reflog.h"
>  #include "refs.h"
>  #include "revision.h"
> -#include "tree-walk.h"
>  #include "worktree.h"
>
> But perhaps some of those are really "needed" but brought in implicitly?
>
>> [...]
>> diff --git a/reflog.h b/reflog.h
>> new file mode 100644
>> index 00000000000..3427021cdc2
>> --- /dev/null
>> +++ b/reflog.h
>> @@ -0,0 +1,49 @@
>> +#ifndef REFLOG_H
>> +#define REFLOG_H
>> +
>> +#include "refs.h"
>
> Just a nit but I think the reflog_delete() should be wrapped (ends up at
> 80 cols), and the usual style in this project is to not whitespace-pad
> so much, i.e. this on top:

sounds good!

>
> diff --git a/reflog.h b/reflog.h
> index 3427021cdc2..d2906fb9f8d 100644
> --- a/reflog.h
> +++ b/reflog.h
> @@ -1,6 +1,5 @@
>  #ifndef REFLOG_H
>  #define REFLOG_H
> -
>  #include "refs.h"
>
>  struct cmd_reflog_expire_cb {
> @@ -25,25 +24,20 @@ struct expire_reflog_policy_cb {
>  	unsigned int dry_run:1;
>  };
>
> -int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose);
> -
> +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 */
>
>
> Other than all that I really can't find anything at all to comment on,
> and I see that all the points raised in previous rounds by others were
> addressed.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH 1/3] reflog: libify delete reflog function and helpers
  2022-02-21  1:50       ` Taylor Blau
@ 2022-02-23 19:50         ` John Cai
  0 siblings, 0 replies; 63+ messages in thread
From: John Cai @ 2022-02-23 19:50 UTC (permalink / raw)
  To: Taylor Blau; +Cc: John Cai via GitGitGadget, git

Hi Taylor,

On 20 Feb 2022, at 20:50, Taylor Blau wrote:

> On Sun, Feb 20, 2022 at 08:43:14PM -0500, John Cai wrote:
>>> One question that I had which I don't see answered already is what the
>>> plan is for existing reflog-related functions that live in refs.h.
>>> Should or will those functions be moved to the new reflog-specific
>>> header, too?
>>
>> Thanks for bringing ths up! Maybe this cleanup can be included in a separate
>> patch series since this one is mainly about getting rid of the shell-out in
>> stash.c. What do you think?
>
> Yeah; I think that's fine. We don't need to get all reflog-related
> functions moved into reflog.h in the first series, so I think it's fine
> to move things as you discover them.
>
> I was just wondering whether it was something you planned to do at some
> point (as part of this series, or a future one).

Yes I will open a new series for this. I've been looking through refs.h, and as you
alluded to there are a number of reflog related functions. But, they also relate to refs.
I'm not sure which ones should go into reflog.h versus staying in refs.h. What are your
thoughts?

>
> Thanks,
> Taylor

Thanks!
John

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23  8:54     ` Ævar Arnfjörð Bjarmason
@ 2022-02-23 21:27       ` Junio C Hamano
  2022-02-23 21:50         ` Ævar Arnfjörð Bjarmason
  2022-02-23 21:50         ` John Cai
  2022-02-23 22:51       ` Junio C Hamano
  1 sibling, 2 replies; 63+ messages in thread
From: Junio C Hamano @ 2022-02-23 21:27 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: John Cai via GitGitGadget, git, Taylor Blau, John Cai

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

> This test was already a bit broken in needing the preceding tests, but
> it will break now if REFFILES isn't true, which you can reproduce
> e.g. with:
>
>     ./t3903-stash.sh --run=1-16,18-50 -vixd
>
> Perhaps the least sucky solution to that is:
>
> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> index ec9cc5646d6..1d11c9bda20 100755
> --- a/t/t3903-stash.sh
> +++ b/t/t3903-stash.sh
> @@ -205,13 +205,19 @@ test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>  	cat >expect <<-EOF &&
>  	$(test_oid zero) $oid
>  	EOF
> -	test_cmp expect actual
> +	test_cmp expect actual &&
> +	>dropped-stash
>  '

If "git stash drop", invoked in earlier part of this test before the
precontext, fails, then test_cmp would fail and we leave
dropped-stash untouched, even though we did run "git stash drop"
already.

Why does the next test need to depend on what has happened earlier?

>  test_expect_success 'stash pop' '
>  	git reset --hard &&
>  	git stash pop &&
> -	test 9 = $(cat file) &&
> +	if test -e dropped-stash
> +	then
> +		test 9 = $(cat file)
> +	else
> +		test 3 = $(cat file)
> +	fi &&
>  	test 1 = $(git show :file) &&
>  	test 1 = $(git show HEAD:file) &&
>  	test 0 = $(git stash list | wc -l)

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 2/3] reflog: libify delete reflog function and helpers
  2022-02-22 18:30   ` [PATCH v2 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
  2022-02-23  9:02     ` Ævar Arnfjörð Bjarmason
@ 2022-02-23 21:28     ` Junio C Hamano
  1 sibling, 0 replies; 63+ messages in thread
From: Junio C Hamano @ 2022-02-23 21:28 UTC (permalink / raw)
  To: John Cai via GitGitGadget
  Cc: git, Ævar Arnfjörð Bjarmason, Taylor Blau,
	John Cai

"John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: John Cai <johncai86@gmail.com>
>
> 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>
> ---
>  Makefile         |   1 +
>  builtin/reflog.c | 451 +----------------------------------------------
>  object.h         |   2 +-
>  reflog.c         | 435 +++++++++++++++++++++++++++++++++++++++++++++
>  reflog.h         |  49 +++++
>  5 files changed, 490 insertions(+), 448 deletions(-)
>  create mode 100644 reflog.c
>  create mode 100644 reflog.h

This round, without polluting global namespace in reflog.h by moving
too many things there and instead keeping what is private to the
implementation in reflog.c, looks much better than the previous
round.

Looking good.

Thanks.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23 21:27       ` Junio C Hamano
@ 2022-02-23 21:50         ` Ævar Arnfjörð Bjarmason
  2022-02-24 18:21           ` John Cai
  2022-02-23 21:50         ` John Cai
  1 sibling, 1 reply; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-23 21:50 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: John Cai via GitGitGadget, git, Taylor Blau, John Cai,
	Han-Wen Nienhuys


On Wed, Feb 23 2022, Junio C Hamano wrote:

> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>> This test was already a bit broken in needing the preceding tests, but
>> it will break now if REFFILES isn't true, which you can reproduce
>> e.g. with:
>>
>>     ./t3903-stash.sh --run=1-16,18-50 -vixd
>>
>> Perhaps the least sucky solution to that is:
>>
>> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
>> index ec9cc5646d6..1d11c9bda20 100755
>> --- a/t/t3903-stash.sh
>> +++ b/t/t3903-stash.sh
>> @@ -205,13 +205,19 @@ test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>  	cat >expect <<-EOF &&
>>  	$(test_oid zero) $oid
>>  	EOF
>> -	test_cmp expect actual
>> +	test_cmp expect actual &&
>> +	>dropped-stash
>>  '
>
> If "git stash drop", invoked in earlier part of this test before the
> precontext, fails, then test_cmp would fail and we leave
> dropped-stash untouched, even though we did run "git stash drop"
> already.

Yes, that's an edge case that's exposed here, but which I thought wasn't
worth bothering with. I.e. if you get such a failure on test N getting
N+1 failing as well isn't that big of a deal.

The big deal is rather that we know we're adding a REFFILES dependency
to this, which won't run this at all, which will make the "stash pop"
below fail.

> Why does the next test need to depend on what has happened earlier?

They don't need to, and ideally wouldn't, but most of our test suite has
this issue already. Try e.g. running it with:

    prove t[0-9]*.sh :: --run=10-20 --immediate

And for this particular file running e.g. this on master:

    ./t3903-stash.sh --run=1-10,30-40

Will fail 7 tests in the 30-40 range.

So while it's ideal that we can combine tests with arbitrary --run
parameters, i.e. all tests would tear down fully, not depend on earlier
tests etc. we're really far from that being the case in practice.

So insisting on some general refactoring of this file as part of this
series seems a bit overzelous, which is why I'm suggesting the bare
minimum to expect and work around the inevitable REFFILES failure, as
Han-Wen is actively working in that area.

>>  test_expect_success 'stash pop' '
>>  	git reset --hard &&
>>  	git stash pop &&
>> -	test 9 = $(cat file) &&
>> +	if test -e dropped-stash
>> +	then
>> +		test 9 = $(cat file)
>> +	else
>> +		test 3 = $(cat file)
>> +	fi &&
>>  	test 1 = $(git show :file) &&
>>  	test 1 = $(git show HEAD:file) &&
>>  	test 0 = $(git stash list | wc -l)


^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23 21:27       ` Junio C Hamano
  2022-02-23 21:50         ` Ævar Arnfjörð Bjarmason
@ 2022-02-23 21:50         ` John Cai
  1 sibling, 0 replies; 63+ messages in thread
From: John Cai @ 2022-02-23 21:50 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Ævar Arnfjörð Bjarmason, John Cai via GitGitGadget,
	git, Taylor Blau

Hi Junio,

On 23 Feb 2022, at 16:27, Junio C Hamano wrote:

> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>> This test was already a bit broken in needing the preceding tests, but
>> it will break now if REFFILES isn't true, which you can reproduce
>> e.g. with:
>>
>>     ./t3903-stash.sh --run=1-16,18-50 -vixd
>>
>> Perhaps the least sucky solution to that is:
>>
>> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
>> index ec9cc5646d6..1d11c9bda20 100755
>> --- a/t/t3903-stash.sh
>> +++ b/t/t3903-stash.sh
>> @@ -205,13 +205,19 @@ test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>  	cat >expect <<-EOF &&
>>  	$(test_oid zero) $oid
>>  	EOF
>> -	test_cmp expect actual
>> +	test_cmp expect actual &&
>> +	>dropped-stash
>>  '
>
> If "git stash drop", invoked in earlier part of this test before the
> precontext, fails, then test_cmp would fail and we leave
> dropped-stash untouched, even though we did run "git stash drop"
> already.
>
> Why does the next test need to depend on what has happened earlier?

Ideally it shouldn't, but it seems like the way these test have been written
makes subsequent tests depend on what previous tests have stashed.

I'm wondering now that while we're at it, if we should just clean up these tests
so there are no dependencies between the tests. Otherwise it's quite painful the
next time someone needs to add a test here.

We could follow the pattern in 5ac15ad2509 (reflog tests: add --updateref tests,
2021-10-16), where Ævar set up the tests and copied over the repo so each test
is isolated from each other.

>
>>  test_expect_success 'stash pop' '
>>  	git reset --hard &&
>>  	git stash pop &&
>> -	test 9 = $(cat file) &&
>> +	if test -e dropped-stash
>> +	then
>> +		test 9 = $(cat file)
>> +	else
>> +		test 3 = $(cat file)
>> +	fi &&
>>  	test 1 = $(git show :file) &&
>>  	test 1 = $(git show HEAD:file) &&
>>  	test 0 = $(git stash list | wc -l)

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23  8:54     ` Ævar Arnfjörð Bjarmason
  2022-02-23 21:27       ` Junio C Hamano
@ 2022-02-23 22:51       ` Junio C Hamano
  2022-02-23 23:12         ` John Cai
  1 sibling, 1 reply; 63+ messages in thread
From: Junio C Hamano @ 2022-02-23 22:51 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: John Cai via GitGitGadget, git, Taylor Blau, John Cai

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

>> +test_expect_success 'drop stash reflog updates refs/stash' '
>> +	git reset --hard &&
>> +	git rev-parse refs/stash >expect &&
>> +	echo 9 >file &&
>> +	git stash &&
>> +	git stash drop stash@{0} &&
>> +	git rev-parse refs/stash >actual &&
>> +	test_cmp expect actual
>> +'
>
> This one will be portable to the reftable backend.
>
>> +test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>
> But as I noted in <220222.86fsob88h7.gmgdl@evledraar.gmail.com> (but it
> was easy to miss) this test will need to depend on REFFILES. So just
> changing this line to:
>
>     test_expect_success REFFILES 'drop stash[...]'
>
>> +	git reset --hard &&
>> +	echo 9 >file &&
>> +	git stash &&
>> +	oid="$(git rev-parse stash@{0})" &&
>> +	git stash drop stash@{1} &&
>> +	cut -d" " -f1-2 .git/logs/refs/stash >actual &&
>> +	cat >expect <<-EOF &&
>> +	$(test_oid zero) $oid
>> +	EOF
>> +	test_cmp expect actual
>> +'

Why should this be tested with "cut" in the first place, though?

If we start from

    stash@{0} = A
    stash@{1} = B
    stash@{2} = C

and after saying "drop stash@{1}", what we need to check is that

    stash@{0} = A
    stash@{1} = C

now holds, which can be done with "git rev-parse", and the fact that
the ref-files backend happens to record both before-and-after object
IDs is an irrelevant implementation detail, no?

I am still puzzled.



^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23 22:51       ` Junio C Hamano
@ 2022-02-23 23:12         ` John Cai
  2022-02-23 23:27           ` Ævar Arnfjörð Bjarmason
  2022-02-23 23:50           ` Junio C Hamano
  0 siblings, 2 replies; 63+ messages in thread
From: John Cai @ 2022-02-23 23:12 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Ævar Arnfjörð Bjarmason, John Cai via GitGitGadget,
	git, Taylor Blau

Hi Junio,

On 23 Feb 2022, at 17:51, Junio C Hamano wrote:

> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>
>>> +test_expect_success 'drop stash reflog updates refs/stash' '
>>> +	git reset --hard &&
>>> +	git rev-parse refs/stash >expect &&
>>> +	echo 9 >file &&
>>> +	git stash &&
>>> +	git stash drop stash@{0} &&
>>> +	git rev-parse refs/stash >actual &&
>>> +	test_cmp expect actual
>>> +'
>>
>> This one will be portable to the reftable backend.
>>
>>> +test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>
>> But as I noted in <220222.86fsob88h7.gmgdl@evledraar.gmail.com> (but it
>> was easy to miss) this test will need to depend on REFFILES. So just
>> changing this line to:
>>
>>     test_expect_success REFFILES 'drop stash[...]'
>>
>>> +	git reset --hard &&
>>> +	echo 9 >file &&
>>> +	git stash &&
>>> +	oid="$(git rev-parse stash@{0})" &&
>>> +	git stash drop stash@{1} &&
>>> +	cut -d" " -f1-2 .git/logs/refs/stash >actual &&
>>> +	cat >expect <<-EOF &&
>>> +	$(test_oid zero) $oid
>>> +	EOF
>>> +	test_cmp expect actual
>>> +'
>
> Why should this be tested with "cut" in the first place, though?
>
> If we start from
>
>     stash@{0} = A
>     stash@{1} = B
>     stash@{2} = C
>
> and after saying "drop stash@{1}", what we need to check is that
>
>     stash@{0} = A
>     stash@{1} = C

Yes, this is true but that doesn't seem to test the --rewrite functionality.
I could be missing something, but it seems that the reflog --rewrite option
will write the LHS old oid value in the .git/logs/refs/stash file. When
--rewrite isn't used, the reflog delete still does the right thing to the
RHS entry.

I couldn't find any way to check this LFS value other than reaching into the
actual file. If there is a way that would be preferable.
>
> now holds, which can be done with "git rev-parse", and the fact that
> the ref-files backend happens to record both before-and-after object
> IDs is an irrelevant implementation detail, no?
>
> I am still puzzled.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23 23:12         ` John Cai
@ 2022-02-23 23:27           ` Ævar Arnfjörð Bjarmason
  2022-02-23 23:50           ` Junio C Hamano
  1 sibling, 0 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-23 23:27 UTC (permalink / raw)
  To: John Cai
  Cc: Junio C Hamano, John Cai via GitGitGadget, git, Taylor Blau,
	Han-Wen Nienhuys


On Wed, Feb 23 2022, John Cai wrote:

> Hi Junio,
>
> On 23 Feb 2022, at 17:51, Junio C Hamano wrote:
>
>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>>
>>>> +test_expect_success 'drop stash reflog updates refs/stash' '
>>>> +	git reset --hard &&
>>>> +	git rev-parse refs/stash >expect &&
>>>> +	echo 9 >file &&
>>>> +	git stash &&
>>>> +	git stash drop stash@{0} &&
>>>> +	git rev-parse refs/stash >actual &&
>>>> +	test_cmp expect actual
>>>> +'
>>>
>>> This one will be portable to the reftable backend.
>>>
>>>> +test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>>
>>> But as I noted in <220222.86fsob88h7.gmgdl@evledraar.gmail.com> (but it
>>> was easy to miss) this test will need to depend on REFFILES. So just
>>> changing this line to:
>>>
>>>     test_expect_success REFFILES 'drop stash[...]'
>>>
>>>> +	git reset --hard &&
>>>> +	echo 9 >file &&
>>>> +	git stash &&
>>>> +	oid="$(git rev-parse stash@{0})" &&
>>>> +	git stash drop stash@{1} &&
>>>> +	cut -d" " -f1-2 .git/logs/refs/stash >actual &&
>>>> +	cat >expect <<-EOF &&
>>>> +	$(test_oid zero) $oid
>>>> +	EOF
>>>> +	test_cmp expect actual
>>>> +'
>>
>> Why should this be tested with "cut" in the first place, though?
>>
>> If we start from
>>
>>     stash@{0} = A
>>     stash@{1} = B
>>     stash@{2} = C
>>
>> and after saying "drop stash@{1}", what we need to check is that
>>
>>     stash@{0} = A
>>     stash@{1} = C
>
> Yes, this is true but that doesn't seem to test the --rewrite functionality.
> I could be missing something, but it seems that the reflog --rewrite option
> will write the LHS old oid value in the .git/logs/refs/stash file. When
> --rewrite isn't used, the reflog delete still does the right thing to the
> RHS entry.
>
> I couldn't find any way to check this LFS value other than reaching into the
> actual file. If there is a way that would be preferable.

Thanks for that summary that's accurate as far as I know. I think that's
how this all works, and I don't know of another way to extract this
information than this reaching behind the curtain.

Which, I think is a lot clearer if we amend the test like this. Note
that this doesn't really add anything for catching a regression goes,
but I think helps guide the human reader through this step-by-step. So
perhaps it would be good to fix the test up to have it (or maybe not):

diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index ec9cc5646d6..bc58e99e3e6 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -198,12 +198,25 @@ test_expect_success 'drop stash reflog updates refs/stash' '
 test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
 	git reset --hard &&
 	echo 9 >file &&
+
+	# Our two stashes
+	old_oid="$(git rev-parse stash@{0})" &&
 	git stash &&
-	oid="$(git rev-parse stash@{0})" &&
+	new_oid="$(git rev-parse stash@{0})" &&
+
+	# Our stash <old oid>/<new oid> before "drop"
+	cat >expect <<-EOF &&
+	$(test_oid zero) $old_oid
+	$old_oid $new_oid
+	EOF
+	cut -d" " -f1-2 .git/logs/refs/stash >actual &&
+	test_cmp expect actual &&
+
+	# Our stash <old oid>/<new oid> after "drop"
 	git stash drop stash@{1} &&
 	cut -d" " -f1-2 .git/logs/refs/stash >actual &&
 	cat >expect <<-EOF &&
-	$(test_oid zero) $oid
+	$(test_oid zero) $new_oid
 	EOF
 	test_cmp expect actual
 '

If this series is amended to drop the "EXPIRE_REFLOGS_REWRITE" flag then
this will fail on that last test_cmp like:
    
    + diff -u expect actual
    --- expect      2022-02-23 23:37:40.438221222 +0000
    +++ actual      2022-02-23 23:37:40.434221258 +0000
    @@ -1 +1 @@
    -0000000000000000000000000000000000000000 236c59f58e239e74e90b6832a98fa4b7f4b33647
    +5c6ad4ca28e71ae3a007e6c77043d04bc42fa9ee 236c59f58e239e74e90b6832a98fa4b7f4b33647

I.e. our <old oid> is now referring to the now-deleted stash entry we
just deleted, since we didn't rewrite it.

And as we can see with some manual inspection the state before we
dropped stash@{1} was:

    0000000000000000000000000000000000000000 5c6ad4ca28e71ae3a007e6c77043d04bc42fa9ee
    5c6ad4ca28e71ae3a007e6c77043d04bc42fa9ee 236c59f58e239e74e90b6832a98fa4b7f4b33647

My usual method of checking my assumption about this not being otherwise
inspectable would be something like:
        
    diff --git a/refs/files-backend.c b/refs/files-backend.c
    index f59589d6cce..590c13e7a2b 100644
    --- a/refs/files-backend.c
    +++ b/refs/files-backend.c
    @@ -3133,7 +3133,7 @@ static int files_reflog_expire(struct ref_store *ref_store,
            const struct object_id *oid;
     
            memset(&cb, 0, sizeof(cb));
    -       cb.rewrite = !!(expire_flags & EXPIRE_REFLOGS_REWRITE);
    +       cb.rewrite = 0;
            cb.dry_run = !!(expire_flags & EXPIRE_REFLOGS_DRY_RUN);
            cb.policy_cb = policy_cb_data;
            cb.should_prune_fn = should_prune_fn;

I.e. let's intentionally break the flag, and see what else fails (it's
set in a few places, but this is the only place where it's checked).

That should normally find the other things that are testing this, maybe
there's a better way.

But, no such luck :) The only thing that'll fail is this new test being
added here.

So just like my 5ac15ad2509 (reflog tests: add --updateref tests,
2021-10-16) this is covering a true blindspot in the "git reflog"
functionality.

The only tests that used --rewrite were a test added in c41a87dd80c
(refs: make rev-parse --quiet actually quiet, 2014-09-18), which will
pass if --rewrite is omitted.

And the ones I added in 5ac15ad2509, which I added not to test --rewrite
per-se, but to test that the --updateref part of it behaved as expected
in combination with whatever effect it was having.

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23 23:12         ` John Cai
  2022-02-23 23:27           ` Ævar Arnfjörð Bjarmason
@ 2022-02-23 23:50           ` Junio C Hamano
  2022-02-24 14:53             ` John Cai
  1 sibling, 1 reply; 63+ messages in thread
From: Junio C Hamano @ 2022-02-23 23:50 UTC (permalink / raw)
  To: John Cai
  Cc: Ævar Arnfjörð Bjarmason, John Cai via GitGitGadget,
	git, Taylor Blau

John Cai <johncai86@gmail.com> writes:

> Yes, this is true but that doesn't seem to test the --rewrite functionality.
> I could be missing something, but it seems that the reflog --rewrite option
> will write the LHS old oid value in the .git/logs/refs/stash file. When
> --rewrite isn't used, the reflog delete still does the right thing to the
> RHS entry.
>
> I couldn't find any way to check this LFS value other than reaching into the
> actual file. If there is a way that would be preferable.

Ah, that one.

As 2b81fab2 (git-reflog: add option --rewrite to update reflog
entries while expiring, 2008-02-22) says, the redundant half of the
reflog entry only matters to "certain sanity checks" and would not
be even visible to normal ref API users.  I wonder why we need to
even say "--rewrite" in the first place.  Perhaps we should
implicitly set it always and eventually deprecate that option.




^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23 23:50           ` Junio C Hamano
@ 2022-02-24 14:53             ` John Cai
  0 siblings, 0 replies; 63+ messages in thread
From: John Cai @ 2022-02-24 14:53 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Ævar Arnfjörð Bjarmason, John Cai via GitGitGadget,
	git, Taylor Blau

Hi Junio,

On 23 Feb 2022, at 18:50, Junio C Hamano wrote:

> John Cai <johncai86@gmail.com> writes:
>
>> Yes, this is true but that doesn't seem to test the --rewrite functionality.
>> I could be missing something, but it seems that the reflog --rewrite option
>> will write the LHS old oid value in the .git/logs/refs/stash file. When
>> --rewrite isn't used, the reflog delete still does the right thing to the
>> RHS entry.
>>
>> I couldn't find any way to check this LFS value other than reaching into the
>> actual file. If there is a way that would be preferable.
>
> Ah, that one.
>
> As 2b81fab2 (git-reflog: add option --rewrite to update reflog
> entries while expiring, 2008-02-22) says, the redundant half of the
> reflog entry only matters to "certain sanity checks" and would not
> be even visible to normal ref API users.  I wonder why we need to
> even say "--rewrite" in the first place.  Perhaps we should
> implicitly set it always and eventually deprecate that option.

Yeah, that makes sense. I had this thought as I was figuring out
how to test this. I can take care of this in a separate patch series

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-23 21:50         ` Ævar Arnfjörð Bjarmason
@ 2022-02-24 18:21           ` John Cai
  2022-02-25 11:45             ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 63+ messages in thread
From: John Cai @ 2022-02-24 18:21 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: Junio C Hamano, John Cai via GitGitGadget, git, Taylor Blau,
	Han-Wen Nienhuys

Hi Ævar,

On 23 Feb 2022, at 16:50, Ævar Arnfjörð Bjarmason wrote:

> On Wed, Feb 23 2022, Junio C Hamano wrote:
>
>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>>
>>> This test was already a bit broken in needing the preceding tests, but
>>> it will break now if REFFILES isn't true, which you can reproduce
>>> e.g. with:
>>>
>>>     ./t3903-stash.sh --run=1-16,18-50 -vixd
>>>
>>> Perhaps the least sucky solution to that is:
>>>
>>> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
>>> index ec9cc5646d6..1d11c9bda20 100755
>>> --- a/t/t3903-stash.sh
>>> +++ b/t/t3903-stash.sh
>>> @@ -205,13 +205,19 @@ test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>>  	cat >expect <<-EOF &&
>>>  	$(test_oid zero) $oid
>>>  	EOF
>>> -	test_cmp expect actual
>>> +	test_cmp expect actual &&
>>> +	>dropped-stash
>>>  '
>>
>> If "git stash drop", invoked in earlier part of this test before the
>> precontext, fails, then test_cmp would fail and we leave
>> dropped-stash untouched, even though we did run "git stash drop"
>> already.
>
> Yes, that's an edge case that's exposed here, but which I thought wasn't
> worth bothering with. I.e. if you get such a failure on test N getting
> N+1 failing as well isn't that big of a deal.
>
> The big deal is rather that we know we're adding a REFFILES dependency
> to this, which won't run this at all, which will make the "stash pop"
> below fail.
>
>> Why does the next test need to depend on what has happened earlier?
>
> They don't need to, and ideally wouldn't, but most of our test suite has
> this issue already. Try e.g. running it with:
>
>     prove t[0-9]*.sh :: --run=10-20 --immediate
>
> And for this particular file running e.g. this on master:
>
>     ./t3903-stash.sh --run=1-10,30-40
>
> Will fail 7 tests in the 30-40 range.
>
> So while it's ideal that we can combine tests with arbitrary --run
> parameters, i.e. all tests would tear down fully, not depend on earlier
> tests etc. we're really far from that being the case in practice.
>
> So insisting on some general refactoring of this file as part of this
> series seems a bit overzelous, which is why I'm suggesting the bare
> minimum to expect and work around the inevitable REFFILES failure, as
> Han-Wen is actively working in that area.

Curious what your thoughts are on an effort to isolate these tests from each other.
I like your approach in t/t1417 in creating a test repo and copying it over each time.
Something like this?

diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index ac345eced8cb..40254f8dc99c 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -41,7 +41,9 @@ diff_cmp () {
        rm -f "$1.compare" "$2.compare"
 }

-test_expect_success 'stash some dirty working directory' '
+test_expect_success 'setup' '
+       git init repo &&
+       cd repo &&
        echo 1 >file &&
        git add file &&
        echo unrelated >other-file &&
@@ -54,48 +56,54 @@ test_expect_success 'stash some dirty working directory' '
        test_tick &&
        git stash &&
        git diff-files --quiet &&
-       git diff-index --cached --quiet HEAD
+       git diff-index --cached --quiet HEAD &&
+       cat >expect <<-EOF &&
+       diff --git a/file b/file
+       index 0cfbf08..00750ed 100644
+       --- a/file
+       +++ b/file
+       @@ -1 +1 @@
+       -2
+       +3
+       EOF
+       cd ../
 '

-cat >expect <<EOF
-diff --git a/file b/file
-index 0cfbf08..00750ed 100644
---- a/file
-+++ b/file
-@@ -1 +1 @@
--2
-+3
-EOF
+test_stash () {
+       cp -R repo copy &&
+       cd copy &&
+       test_expect_success "$@" &&
+       cd ../ &&
+       rm -rf copy
+}

-test_expect_success 'parents of stash' '
+test_stash 'parents of stash' '
        test $(git rev-parse stash^) = $(git rev-parse HEAD) &&
        git diff stash^2..stash >output &&
        diff_cmp expect output
 '

Not sure if it's worth it though?


>
>>>  test_expect_success 'stash pop' '
>>>  	git reset --hard &&
>>>  	git stash pop &&
>>> -	test 9 = $(cat file) &&
>>> +	if test -e dropped-stash
>>> +	then
>>> +		test 9 = $(cat file)
>>> +	else
>>> +		test 3 = $(cat file)
>>> +	fi &&
>>>  	test 1 = $(git show :file) &&
>>>  	test 1 = $(git show HEAD:file) &&
>>>  	test 0 = $(git stash list | wc -l)

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-24 18:21           ` John Cai
@ 2022-02-25 11:45             ` Ævar Arnfjörð Bjarmason
  2022-02-25 17:23               ` Junio C Hamano
  0 siblings, 1 reply; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-02-25 11:45 UTC (permalink / raw)
  To: John Cai
  Cc: Junio C Hamano, John Cai via GitGitGadget, git, Taylor Blau,
	Han-Wen Nienhuys


On Thu, Feb 24 2022, John Cai wrote:

> Hi Ævar,
>
> On 23 Feb 2022, at 16:50, Ævar Arnfjörð Bjarmason wrote:
>
>> On Wed, Feb 23 2022, Junio C Hamano wrote:
>>
>>> Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:
>>>
>>>> This test was already a bit broken in needing the preceding tests, but
>>>> it will break now if REFFILES isn't true, which you can reproduce
>>>> e.g. with:
>>>>
>>>>     ./t3903-stash.sh --run=1-16,18-50 -vixd
>>>>
>>>> Perhaps the least sucky solution to that is:
>>>>
>>>> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
>>>> index ec9cc5646d6..1d11c9bda20 100755
>>>> --- a/t/t3903-stash.sh
>>>> +++ b/t/t3903-stash.sh
>>>> @@ -205,13 +205,19 @@ test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>>>  	cat >expect <<-EOF &&
>>>>  	$(test_oid zero) $oid
>>>>  	EOF
>>>> -	test_cmp expect actual
>>>> +	test_cmp expect actual &&
>>>> +	>dropped-stash
>>>>  '
>>>
>>> If "git stash drop", invoked in earlier part of this test before the
>>> precontext, fails, then test_cmp would fail and we leave
>>> dropped-stash untouched, even though we did run "git stash drop"
>>> already.
>>
>> Yes, that's an edge case that's exposed here, but which I thought wasn't
>> worth bothering with. I.e. if you get such a failure on test N getting
>> N+1 failing as well isn't that big of a deal.
>>
>> The big deal is rather that we know we're adding a REFFILES dependency
>> to this, which won't run this at all, which will make the "stash pop"
>> below fail.
>>
>>> Why does the next test need to depend on what has happened earlier?
>>
>> They don't need to, and ideally wouldn't, but most of our test suite has
>> this issue already. Try e.g. running it with:
>>
>>     prove t[0-9]*.sh :: --run=10-20 --immediate
>>
>> And for this particular file running e.g. this on master:
>>
>>     ./t3903-stash.sh --run=1-10,30-40
>>
>> Will fail 7 tests in the 30-40 range.
>>
>> So while it's ideal that we can combine tests with arbitrary --run
>> parameters, i.e. all tests would tear down fully, not depend on earlier
>> tests etc. we're really far from that being the case in practice.
>>
>> So insisting on some general refactoring of this file as part of this
>> series seems a bit overzelous, which is why I'm suggesting the bare
>> minimum to expect and work around the inevitable REFFILES failure, as
>> Han-Wen is actively working in that area.
>
> Curious what your thoughts are on an effort to isolate these tests from each other.
> I like your approach in t/t1417 in creating a test repo and copying it over each time.
> Something like this?

That looks good to me if you're willing to do that legwork, probably
better in a preceding cleanup commit.

> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> index ac345eced8cb..40254f8dc99c 100755
> --- a/t/t3903-stash.sh
> +++ b/t/t3903-stash.sh
> @@ -41,7 +41,9 @@ diff_cmp () {
>         rm -f "$1.compare" "$2.compare"
>  }
>
> -test_expect_success 'stash some dirty working directory' '
> +test_expect_success 'setup' '
> +       git init repo &&
> +       cd repo &&
>         echo 1 >file &&
>         git add file &&
>         echo unrelated >other-file &&
> @@ -54,48 +56,54 @@ test_expect_success 'stash some dirty working directory' '
>         test_tick &&
>         git stash &&
>         git diff-files --quiet &&
> -       git diff-index --cached --quiet HEAD
> +       git diff-index --cached --quiet HEAD &&
> +       cat >expect <<-EOF &&

nit: you can add \ to that, i.e. <<-\EOF. Helps readability, i.e.  it's
obvious right away that no variables are in play..

> +       diff --git a/file b/file
> +       index 0cfbf08..00750ed 100644
> +       --- a/file
> +       +++ b/file
> +       @@ -1 +1 @@
> +       -2
> +       +3
> +       EOF
> +       cd ../
>  '
>
> -cat >expect <<EOF
> -diff --git a/file b/file
> -index 0cfbf08..00750ed 100644
> ---- a/file
> -+++ b/file
> -@@ -1 +1 @@
> --2
> -+3
> -EOF
> +test_stash () {
> +       cp -R repo copy &&
> +       cd copy &&
> +       test_expect_success "$@" &&
> +       cd ../ &&
> +       rm -rf copy
> +}
>
>
> -test_expect_success 'parents of stash' '
> +test_stash 'parents of stash' '
>         test $(git rev-parse stash^) = $(git rev-parse HEAD) &&
>         git diff stash^2..stash >output &&
>         diff_cmp expect output
>  '

For this sort of thing I think it's usually better to override
"test_expect_success" as a last resort, i.e. to have that
"test_setup_stash_copy" just be a "setup_stash" or whatever function
called from within your test_expect_success.

And instead of the "rm -rf" later, just do:

    test_when_finished "rm -rf copy" &&
    cp -R repo copy &&
    [...]

The test still needs to deal with the sub-repo, but it could cd or use
"-C".

It's bad to add "cd .." in a &&-chain, because if earlier steps fail
we're in the wrong directory for the next test, so either -C or a
sub-shell...

> Not sure if it's worth it though?

Maybe not, which is why I suggested upthread to maybe go for some
smallest possible change here and focus on the lib-ificitaion :)

>
>
>>
>>>>  test_expect_success 'stash pop' '
>>>>  	git reset --hard &&
>>>>  	git stash pop &&
>>>> -	test 9 = $(cat file) &&
>>>> +	if test -e dropped-stash
>>>> +	then
>>>> +		test 9 = $(cat file)
>>>> +	else
>>>> +		test 3 = $(cat file)
>>>> +	fi &&
>>>>  	test 1 = $(git show :file) &&
>>>>  	test 1 = $(git show HEAD:file) &&
>>>>  	test 0 = $(git stash list | wc -l)


^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior
  2022-02-25 11:45             ` Ævar Arnfjörð Bjarmason
@ 2022-02-25 17:23               ` Junio C Hamano
  0 siblings, 0 replies; 63+ messages in thread
From: Junio C Hamano @ 2022-02-25 17:23 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason
  Cc: John Cai, John Cai via GitGitGadget, git, Taylor Blau,
	Han-Wen Nienhuys

Ævar Arnfjörð Bjarmason <avarab@gmail.com> writes:

>> Curious what your thoughts are on an effort to isolate these tests from each other.
>> I like your approach in t/t1417 in creating a test repo and copying it over each time.
>> Something like this?
>
> That looks good to me if you're willing to do that legwork, probably
> better in a preceding cleanup commit.

Yup.  Thanks for helping other contributors.  I agree with many
things you said in your review.

>> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
>> index ac345eced8cb..40254f8dc99c 100755
>> --- a/t/t3903-stash.sh
>> +++ b/t/t3903-stash.sh
>> @@ -41,7 +41,9 @@ diff_cmp () {
>>         rm -f "$1.compare" "$2.compare"
>>  }
>>
>> -test_expect_success 'stash some dirty working directory' '
>> +test_expect_success 'setup' '
>> +       git init repo &&
>> +       cd repo &&

We do not want to "chdir" around without isolating it in a
subprocess.  If this test fails after it goes to "repo" but before it
does "cd ..", the next test begins in the "repo" directory, but it
is most likely not expecting that.

>> -cat >expect <<EOF
>> -diff --git a/file b/file
>> -index 0cfbf08..00750ed 100644
>> ---- a/file
>> -+++ b/file
>> -@@ -1 +1 @@
>> --2
>> -+3
>> -EOF
>> +test_stash () {
>> +       cp -R repo copy &&
>> +       cd copy &&
>> +       test_expect_success "$@" &&
>> +       cd ../ &&
>> +       rm -rf copy
>> +}

This will create an anti-pattern, because you would want to have the
part between "cd copy" and "cd .." in a subshell, but you do not
want to do test_expect_success inside a subshell.  Hence, this is a
bad helper that does not help and should not be used, I would think.

>> -test_expect_success 'parents of stash' '
>> +test_stash 'parents of stash' '
>>         test $(git rev-parse stash^) = $(git rev-parse HEAD) &&
>>         git diff stash^2..stash >output &&
>>         diff_cmp expect output
>>  '
>
> For this sort of thing I think it's usually better to override
> "test_expect_success" as a last resort, i.e. to have that
> "test_setup_stash_copy" just be a "setup_stash" or whatever function
> called from within your test_expect_success.
>
> And instead of the "rm -rf" later, just do:
>
>     test_when_finished "rm -rf copy" &&
>     cp -R repo copy &&
>     [...]

Yup.  I think this is how we would write it:

	test_expect_success 'parents of stash' '
		test_when_finished "rm -fr copy" &&
		cp -R repo copy &&
		(
			cd copy &&
			... the real body of the test here, like ...
			test $(git rev-parse stash^) = $(git rev-parse HEAD) &&
		)
	'

> The test still needs to deal with the sub-repo, but it could cd or use
> "-C".

I am not sure about this.  test_expect_success does not take "-C".

^ permalink raw reply	[flat|nested] 63+ messages in thread

* [PATCH v3 0/3] libify reflog
  2022-02-22 18:30 ` [PATCH v2 " John Cai via GitGitGadget
                     ` (2 preceding siblings ...)
  2022-02-22 18:30   ` [PATCH v2 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
@ 2022-02-25 19:30   ` John Cai via GitGitGadget
  2022-02-25 19:30     ` [PATCH v3 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
                       ` (4 more replies)
  3 siblings, 5 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-25 19:30 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai

In [1], there was a discussion around a bug report of stash not recovering
in the middle of the process when killed with ctl-c. It turned out to not be
a bug we need to fix. However, out of that discussion came the idea of
libifying reflog. This can stand alone as a code improvement.

stash.c currently shells out to call reflog to delete reflogs. Libify reflog
delete and call it from both builtin/reflog.c and builtin/stash.c.

This patch has three parts:

 * add missing test coverage for git stash delete
 * libify reflog's delete functionality and move some of the helpers into a
   reflog.c library and call reflog_delete from builtin/reflog.c
 * call reflog_delete from builtin/stash.c

Updates since v2:

 * removed unnecessary includes
 * adjusted wrapping/whitespace in reflog.h
 * adjusted test to be isolated from other tests since currently tests for
   stash depend on each other. There was some discussion around this and
   even a possibility to refactor the tests. However, it would have been a
   larger effort than is worth for this series, so instead I just made one
   of the tests I added be isolated from the others.

Updates since v1:

 * added missing test coverage
 * squashed 1/3 and 2/3 together
 * moved enum into reflog.c
 * updated object.h's flag allocation mapping

 1. https://lore.kernel.org/git/220126.86h79qe692.gmgdl@evledraar.gmail.com/

John Cai (3):
  stash: add tests to ensure reflog --rewrite --updatref behavior
  reflog: libify delete reflog function and helpers
  stash: call reflog_delete() in reflog.c

 Makefile         |   1 +
 builtin/reflog.c | 455 +----------------------------------------------
 builtin/stash.c  |  18 +-
 object.h         |   2 +-
 reflog.c         | 432 ++++++++++++++++++++++++++++++++++++++++++++
 reflog.h         |  43 +++++
 t/t3903-stash.sh |  65 +++++--
 7 files changed, 539 insertions(+), 477 deletions(-)
 create mode 100644 reflog.c
 create mode 100644 reflog.h


base-commit: e6ebfd0e8cbbd10878070c8a356b5ad1b3ca464e
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1218%2Fjohn-cai%2Fjc-libify-reflog-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1218/john-cai/jc-libify-reflog-v3
Pull-Request: https://github.com/git/git/pull/1218

Range-diff vs v2:

 1:  6e136b62ca4 ! 1:  33299825fc4 stash: add test to ensure reflog --rewrite --updatref behavior
     @@ Metadata
      Author: John Cai <johncai86@gmail.com>
      
       ## Commit message ##
     -    stash: add test to ensure reflog --rewrite --updatref behavior
     +    stash: add tests to ensure reflog --rewrite --updatref behavior
      
          There is missing test coverage to ensure that the resulting reflogs
          after a git stash drop has had its old oid rewritten if applicable, and
     @@ Commit message
          Signed-off-by: John Cai <johncai86@gmail.com>
      
       ## t/t3903-stash.sh ##
     +@@ t/t3903-stash.sh: diff_cmp () {
     + 	rm -f "$1.compare" "$2.compare"
     + }
     + 
     +-test_expect_success 'stash some dirty working directory' '
     +-	echo 1 >file &&
     +-	git add file &&
     +-	echo unrelated >other-file &&
     +-	git add other-file &&
     ++setup_stash() {
     ++	repo_dir=$1
     ++	if test -z $repo_dir; then
     ++		repo_dir="."
     ++	fi
     ++
     ++	echo 1 >$repo_dir/file &&
     ++	git -C $repo_dir add file &&
     ++	echo unrelated >$repo_dir/other-file &&
     ++	git -C $repo_dir add other-file &&
     + 	test_tick &&
     +-	git commit -m initial &&
     +-	echo 2 >file &&
     ++	git -C $repo_dir commit -m initial &&
     ++	echo 2 >$repo_dir/file &&
     + 	git add file &&
     +-	echo 3 >file &&
     ++	echo 3 >$repo_dir/file &&
     + 	test_tick &&
     +-	git stash &&
     +-	git diff-files --quiet &&
     +-	git diff-index --cached --quiet HEAD
     ++	git -C $repo_dir stash &&
     ++	git -C $repo_dir diff-files --quiet &&
     ++	git -C $repo_dir diff-index --cached --quiet HEAD
     ++}
     ++
     ++test_expect_success 'stash some dirty working directory' '
     ++	setup_stash
     + '
     + 
     + cat >expect <<EOF
      @@ t/t3903-stash.sh: test_expect_success 'drop middle stash by index' '
       	test 1 = $(git show HEAD:file)
       '
     @@ t/t3903-stash.sh: test_expect_success 'drop middle stash by index' '
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
     -+	git reset --hard &&
     -+	echo 9 >file &&
     -+	git stash &&
     -+	oid="$(git rev-parse stash@{0})" &&
     -+	git stash drop stash@{1} &&
     -+	cut -d" " -f1-2 .git/logs/refs/stash >actual &&
     ++test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
     ++	git init repo &&
     ++	setup_stash repo &&
     ++	echo 9 >repo/file &&
     ++
     ++	old_oid="$(git -C repo rev-parse stash@{0})" &&
     ++	git -C repo stash &&
     ++	new_oid="$(git -C repo rev-parse stash@{0})" &&
     ++
     ++	cat >expect <<-EOF &&
     ++	$(test_oid zero) $old_oid
     ++	$old_oid $new_oid
     ++	EOF
     ++	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
     ++	test_cmp expect actual &&
     ++
     ++	git -C repo stash drop stash@{1} &&
     ++	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
      +	cat >expect <<-EOF &&
     -+	$(test_oid zero) $oid
     ++	$(test_oid zero) $new_oid
      +	EOF
      +	test_cmp expect actual
      +'
     @@ t/t3903-stash.sh: test_expect_success 'drop middle stash by index' '
       test_expect_success 'stash pop' '
       	git reset --hard &&
       	git stash pop &&
     --	test 3 = $(cat file) &&
     -+	test 9 = $(cat file) &&
     - 	test 1 = $(git show :file) &&
     - 	test 1 = $(git show HEAD:file) &&
     - 	test 0 = $(git stash list | wc -l)
 2:  e7c950218b1 ! 2:  33adfee4ca6 reflog: libify delete reflog function and helpers
     @@ builtin/reflog.c
      @@
       #include "builtin.h"
       #include "config.h"
     - #include "lockfile.h"
     +-#include "lockfile.h"
      -#include "object-store.h"
     - #include "repository.h"
     +-#include "repository.h"
      -#include "commit.h"
      -#include "refs.h"
     - #include "dir.h"
     +-#include "dir.h"
      -#include "tree-walk.h"
     - #include "diff.h"
     +-#include "diff.h"
       #include "revision.h"
       #include "reachable.h"
       #include "worktree.h"
     @@ object.h: struct object_array {
       ## reflog.c (new) ##
      @@
      +#include "cache.h"
     -+#include "commit.h"
      +#include "object-store.h"
     -+#include "reachable.h"
      +#include "reflog.h"
      +#include "refs.h"
      +#include "revision.h"
     -+#include "tree-walk.h"
      +#include "worktree.h"
      +
      +/* Remember to update object flag allocation in object.h */
     @@ reflog.h (new)
      @@
      +#ifndef REFLOG_H
      +#define REFLOG_H
     -+
      +#include "refs.h"
      +
      +struct cmd_reflog_expire_cb {
     @@ reflog.h (new)
      +	unsigned int dry_run:1;
      +};
      +
     -+int reflog_delete(const char *rev, enum expire_reflog_flags flags, int verbose);
     -+
     ++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 */
 3:  a023a70092b = 3:  b17d8e5d43a stash: call reflog_delete() in reflog.c

-- 
gitgitgadget

^ permalink raw reply	[flat|nested] 63+ messages in thread

* [PATCH v3 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-02-25 19:30   ` [PATCH v3 0/3] libify reflog John Cai via GitGitGadget
@ 2022-02-25 19:30     ` John Cai via GitGitGadget
  2022-03-02 18:52       ` Ævar Arnfjörð Bjarmason
  2022-02-25 19:30     ` [PATCH v3 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
                       ` (3 subsequent siblings)
  4 siblings, 1 reply; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-25 19:30 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

There is missing test coverage to ensure that the resulting reflogs
after a git stash drop has had its old oid rewritten if applicable, and
if the refs/stash has been updated if applicable.

Add two tests that verify both of these happen.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
---
 t/t3903-stash.sh | 65 ++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 54 insertions(+), 11 deletions(-)

diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index b149e2af441..73785cf862f 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -41,20 +41,29 @@ diff_cmp () {
 	rm -f "$1.compare" "$2.compare"
 }
 
-test_expect_success 'stash some dirty working directory' '
-	echo 1 >file &&
-	git add file &&
-	echo unrelated >other-file &&
-	git add other-file &&
+setup_stash() {
+	repo_dir=$1
+	if test -z $repo_dir; then
+		repo_dir="."
+	fi
+
+	echo 1 >$repo_dir/file &&
+	git -C $repo_dir add file &&
+	echo unrelated >$repo_dir/other-file &&
+	git -C $repo_dir add other-file &&
 	test_tick &&
-	git commit -m initial &&
-	echo 2 >file &&
+	git -C $repo_dir commit -m initial &&
+	echo 2 >$repo_dir/file &&
 	git add file &&
-	echo 3 >file &&
+	echo 3 >$repo_dir/file &&
 	test_tick &&
-	git stash &&
-	git diff-files --quiet &&
-	git diff-index --cached --quiet HEAD
+	git -C $repo_dir stash &&
+	git -C $repo_dir diff-files --quiet &&
+	git -C $repo_dir diff-index --cached --quiet HEAD
+}
+
+test_expect_success 'stash some dirty working directory' '
+	setup_stash
 '
 
 cat >expect <<EOF
@@ -185,6 +194,40 @@ test_expect_success 'drop middle stash by index' '
 	test 1 = $(git show HEAD:file)
 '
 
+test_expect_success 'drop stash reflog updates refs/stash' '
+	git reset --hard &&
+	git rev-parse refs/stash >expect &&
+	echo 9 >file &&
+	git stash &&
+	git stash drop stash@{0} &&
+	git rev-parse refs/stash >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
+	git init repo &&
+	setup_stash repo &&
+	echo 9 >repo/file &&
+
+	old_oid="$(git -C repo rev-parse stash@{0})" &&
+	git -C repo stash &&
+	new_oid="$(git -C repo rev-parse stash@{0})" &&
+
+	cat >expect <<-EOF &&
+	$(test_oid zero) $old_oid
+	$old_oid $new_oid
+	EOF
+	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
+	test_cmp expect actual &&
+
+	git -C repo stash drop stash@{1} &&
+	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
+	cat >expect <<-EOF &&
+	$(test_oid zero) $new_oid
+	EOF
+	test_cmp expect actual
+'
+
 test_expect_success 'stash pop' '
 	git reset --hard &&
 	git stash pop &&
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 63+ messages in thread

* [PATCH v3 2/3] reflog: libify delete reflog function and helpers
  2022-02-25 19:30   ` [PATCH v3 0/3] libify reflog John Cai via GitGitGadget
  2022-02-25 19:30     ` [PATCH v3 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
@ 2022-02-25 19:30     ` John Cai via GitGitGadget
  2022-02-25 19:30     ` [PATCH v3 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
                       ` (2 subsequent siblings)
  4 siblings, 0 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-25 19:30 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

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>
---
 Makefile         |   1 +
 builtin/reflog.c | 455 +----------------------------------------------
 object.h         |   2 +-
 reflog.c         | 432 ++++++++++++++++++++++++++++++++++++++++++++
 reflog.h         |  43 +++++
 5 files changed, 481 insertions(+), 452 deletions(-)
 create mode 100644 reflog.c
 create mode 100644 reflog.h

diff --git a/Makefile b/Makefile
index 6f0b4b775fe..876d4dfd6cb 100644
--- 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
diff --git a/builtin/reflog.c b/builtin/reflog.c
index 85b838720c3..940db196f62 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -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;
 }
 
diff --git a/object.h b/object.h
index cb556ab7753..a2219464c2b 100644
--- 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
index 00000000000..333fd8708fe
--- /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
index 00000000000..d2906fb9f8d
--- /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 */
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 63+ messages in thread

* [PATCH v3 3/3] stash: call reflog_delete() in reflog.c
  2022-02-25 19:30   ` [PATCH v3 0/3] libify reflog John Cai via GitGitGadget
  2022-02-25 19:30     ` [PATCH v3 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
  2022-02-25 19:30     ` [PATCH v3 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
@ 2022-02-25 19:30     ` John Cai via GitGitGadget
  2022-02-25 19:38     ` [PATCH v3 0/3] libify reflog Taylor Blau
  2022-03-02 22:27     ` [PATCH v4 " John Cai via GitGitGadget
  4 siblings, 0 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-02-25 19:30 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

Now that cmd_reflog_delete has been libified an exported it into a new
reflog.c library so we can call it directly from builtin/stash.c. This
not only gives us a performance gain since we don't need to create a
subprocess, but it also allows us to use the ref transactions api in the
future.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
---
 builtin/stash.c | 18 ++++--------------
 1 file changed, 4 insertions(+), 14 deletions(-)

diff --git a/builtin/stash.c b/builtin/stash.c
index 5897febfbec..3e2f478b761 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -17,6 +17,7 @@
 #include "diffcore.h"
 #include "exec-cmd.h"
 #include "entry.h"
+#include "reflog.h"
 
 #define INCLUDE_ALL_FILES 2
 
@@ -634,20 +635,9 @@ static int reflog_is_empty(const char *refname)
 
 static int do_drop_stash(struct stash_info *info, int quiet)
 {
-	int ret;
-	struct child_process cp_reflog = CHILD_PROCESS_INIT;
-
-	/*
-	 * reflog does not provide a simple function for deleting refs. One will
-	 * need to be added to avoid implementing too much reflog code here
-	 */
-
-	cp_reflog.git_cmd = 1;
-	strvec_pushl(&cp_reflog.args, "reflog", "delete", "--updateref",
-		     "--rewrite", NULL);
-	strvec_push(&cp_reflog.args, info->revision.buf);
-	ret = run_command(&cp_reflog);
-	if (!ret) {
+	if (!reflog_delete(info->revision.buf,
+			   EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,
+			   0)) {
 		if (!quiet)
 			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
 				  oid_to_hex(&info->w_commit));
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH v3 0/3] libify reflog
  2022-02-25 19:30   ` [PATCH v3 0/3] libify reflog John Cai via GitGitGadget
                       ` (2 preceding siblings ...)
  2022-02-25 19:30     ` [PATCH v3 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
@ 2022-02-25 19:38     ` Taylor Blau
  2022-03-02 16:43       ` John Cai
  2022-03-02 22:27     ` [PATCH v4 " John Cai via GitGitGadget
  4 siblings, 1 reply; 63+ messages in thread
From: Taylor Blau @ 2022-02-25 19:38 UTC (permalink / raw)
  To: John Cai via GitGitGadget
  Cc: git, Ævar Arnfjörð Bjarmason, John Cai

On Fri, Feb 25, 2022 at 07:30:49PM +0000, John Cai via GitGitGadget wrote:
> John Cai (3):
>   stash: add tests to ensure reflog --rewrite --updatref behavior
>   reflog: libify delete reflog function and helpers
>   stash: call reflog_delete() in reflog.c
>
>  Makefile         |   1 +
>  builtin/reflog.c | 455 +----------------------------------------------
>  builtin/stash.c  |  18 +-
>  object.h         |   2 +-
>  reflog.c         | 432 ++++++++++++++++++++++++++++++++++++++++++++
>  reflog.h         |  43 +++++
>  t/t3903-stash.sh |  65 +++++--
>  7 files changed, 539 insertions(+), 477 deletions(-)
>  create mode 100644 reflog.c
>  create mode 100644 reflog.h

Thanks; I glossed over the discussion about tests (since it looks like
you and Ævar already have a good handle on how things are going there).

The rest of this version of the series (which I looked at more closely)
looks good to me.

Thanks
Taylor

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v3 0/3] libify reflog
  2022-02-25 19:38     ` [PATCH v3 0/3] libify reflog Taylor Blau
@ 2022-03-02 16:43       ` John Cai
  2022-03-02 18:55         ` Ævar Arnfjörð Bjarmason
  0 siblings, 1 reply; 63+ messages in thread
From: John Cai @ 2022-03-02 16:43 UTC (permalink / raw)
  To: git
  Cc: John Cai via GitGitGadget, Ævar Arnfjörð Bjarmason,
	Junio C Hamano, Taylor Blau

Hi,

Just wanted to bump this thread. It'd be good to get another ack on these
last set of changes.

On 25 Feb 2022, at 14:38, Taylor Blau wrote:

> On Fri, Feb 25, 2022 at 07:30:49PM +0000, John Cai via GitGitGadget wrote:
>> John Cai (3):
>>   stash: add tests to ensure reflog --rewrite --updatref behavior
>>   reflog: libify delete reflog function and helpers
>>   stash: call reflog_delete() in reflog.c
>>
>>  Makefile         |   1 +
>>  builtin/reflog.c | 455 +----------------------------------------------
>>  builtin/stash.c  |  18 +-
>>  object.h         |   2 +-
>>  reflog.c         | 432 ++++++++++++++++++++++++++++++++++++++++++++
>>  reflog.h         |  43 +++++
>>  t/t3903-stash.sh |  65 +++++--
>>  7 files changed, 539 insertions(+), 477 deletions(-)
>>  create mode 100644 reflog.c
>>  create mode 100644 reflog.h
>
> Thanks; I glossed over the discussion about tests (since it looks like
> you and Ævar already have a good handle on how things are going there).
>
> The rest of this version of the series (which I looked at more closely)
> looks good to me.
>
> Thanks
> Taylor

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v3 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-02-25 19:30     ` [PATCH v3 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
@ 2022-03-02 18:52       ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-03-02 18:52 UTC (permalink / raw)
  To: John Cai via GitGitGadget; +Cc: git, Taylor Blau, John Cai


On Fri, Feb 25 2022, John Cai via GitGitGadget wrote:

> From: John Cai <johncai86@gmail.com>
>
> There is missing test coverage to ensure that the resulting reflogs
> after a git stash drop has had its old oid rewritten if applicable, and
> if the refs/stash has been updated if applicable.
>
> Add two tests that verify both of these happen.
>
> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> Signed-off-by: John Cai <johncai86@gmail.com>
> ---
>  t/t3903-stash.sh | 65 ++++++++++++++++++++++++++++++++++++++++--------
>  1 file changed, 54 insertions(+), 11 deletions(-)
>
> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> index b149e2af441..73785cf862f 100755
> --- a/t/t3903-stash.sh
> +++ b/t/t3903-stash.sh
> @@ -41,20 +41,29 @@ diff_cmp () {
>  	rm -f "$1.compare" "$2.compare"
>  }
>  
> -test_expect_success 'stash some dirty working directory' '
> -	echo 1 >file &&
> -	git add file &&
> -	echo unrelated >other-file &&
> -	git add other-file &&
> +setup_stash() {
> +	repo_dir=$1
> +	if test -z $repo_dir; then
> +		repo_dir="."
> +	fi
> +
> +	echo 1 >$repo_dir/file &&
> +	git -C $repo_dir add file &&
> +	echo unrelated >$repo_dir/other-file &&
> +	git -C $repo_dir add other-file &&
>  	test_tick &&
> -	git commit -m initial &&
> -	echo 2 >file &&
> +	git -C $repo_dir commit -m initial &&
> +	echo 2 >$repo_dir/file &&
>  	git add file &&
> -	echo 3 >file &&
> +	echo 3 >$repo_dir/file &&
>  	test_tick &&
> -	git stash &&
> -	git diff-files --quiet &&
> -	git diff-index --cached --quiet HEAD
> +	git -C $repo_dir stash &&
> +	git -C $repo_dir diff-files --quiet &&
> +	git -C $repo_dir diff-index --cached --quiet HEAD
> +}
> +
> +test_expect_success 'stash some dirty working directory' '
> +	setup_stash
>  '
>  
>  cat >expect <<EOF
> @@ -185,6 +194,40 @@ test_expect_success 'drop middle stash by index' '
>  	test 1 = $(git show HEAD:file)
>  '
>  
> +test_expect_success 'drop stash reflog updates refs/stash' '
> +	git reset --hard &&
> +	git rev-parse refs/stash >expect &&
> +	echo 9 >file &&
> +	git stash &&
> +	git stash drop stash@{0} &&
> +	git rev-parse refs/stash >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
> +	git init repo &&
> +	setup_stash repo &&
> +	echo 9 >repo/file &&
> +
> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
> +	git -C repo stash &&
> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
> +
> +	cat >expect <<-EOF &&
> +	$(test_oid zero) $old_oid
> +	$old_oid $new_oid
> +	EOF
> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
> +	test_cmp expect actual &&
> +
> +	git -C repo stash drop stash@{1} &&
> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
> +	cat >expect <<-EOF &&
> +	$(test_oid zero) $new_oid
> +	EOF
> +	test_cmp expect actual
> +'
> +
>  test_expect_success 'stash pop' '
>  	git reset --hard &&
>  	git stash pop &&

I just looked at the post-image of this and didn't notice it was altered
existing code at first, so I was going to suggest just this:
	
	diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
	index 73785cf862f..422d1f7a306 100755
	--- a/t/t3903-stash.sh
	+++ b/t/t3903-stash.sh
	@@ -42,24 +42,14 @@ diff_cmp () {
	 }
	 
	 setup_stash() {
	-	repo_dir=$1
	-	if test -z $repo_dir; then
	-		repo_dir="."
	-	fi
	-
	-	echo 1 >$repo_dir/file &&
	-	git -C $repo_dir add file &&
	-	echo unrelated >$repo_dir/other-file &&
	-	git -C $repo_dir add other-file &&
	-	test_tick &&
	-	git -C $repo_dir commit -m initial &&
	-	echo 2 >$repo_dir/file &&
	+	test_commit initial file 1 &&
	+	test_commit second other-file unrelated &&
	+	echo 2 >file &&
	 	git add file &&
	-	echo 3 >$repo_dir/file &&
	-	test_tick &&
	-	git -C $repo_dir stash &&
	-	git -C $repo_dir diff-files --quiet &&
	-	git -C $repo_dir diff-index --cached --quiet HEAD
	+	echo 3 >file &&
	+	git stash &&
	+	git diff-files --quiet &&
	+	git diff-index --cached --quiet HEAD
	 }
	 
	 test_expect_success 'stash some dirty working directory' '
	@@ -206,7 +196,10 @@ test_expect_success 'drop stash reflog updates refs/stash' '
	 
	 test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
	 	git init repo &&
	-	setup_stash repo &&
	+	(
	+		cd repo &&
	+		setup_stash repo
	+	) &&
	 	echo 9 >repo/file &&
	 
	 	old_oid="$(git -C repo rev-parse stash@{0})" &&

But I think much better as a replacement is:
	
	diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
	index b149e2af441..9dd05a497bc 100755
	--- a/t/t3903-stash.sh
	+++ b/t/t3903-stash.sh
	@@ -41,7 +41,7 @@ diff_cmp () {
	 	rm -f "$1.compare" "$2.compare"
	 }
	 
	-test_expect_success 'stash some dirty working directory' '
	+setup_stash() {
	 	echo 1 >file &&
	 	git add file &&
	 	echo unrelated >other-file &&
	@@ -55,6 +55,10 @@ test_expect_success 'stash some dirty working directory' '
	 	git stash &&
	 	git diff-files --quiet &&
	 	git diff-index --cached --quiet HEAD
	+}
	+
	+test_expect_success 'stash some dirty working directory' '
	+	setup_stash
	 '
	 
	 cat >expect <<EOF
	@@ -185,6 +189,43 @@ test_expect_success 'drop middle stash by index' '
	 	test 1 = $(git show HEAD:file)
	 '
	 
	+test_expect_success 'drop stash reflog updates refs/stash' '
	+	git reset --hard &&
	+	git rev-parse refs/stash >expect &&
	+	echo 9 >file &&
	+	git stash &&
	+	git stash drop stash@{0} &&
	+	git rev-parse refs/stash >actual &&
	+	test_cmp expect actual
	+'
	+
	+test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
	+	git init repo &&
	+	(
	+		cd repo &&
	+		setup_stash repo
	+	) &&
	+	echo 9 >repo/file &&
	+
	+	old_oid="$(git -C repo rev-parse stash@{0})" &&
	+	git -C repo stash &&
	+	new_oid="$(git -C repo rev-parse stash@{0})" &&
	+
	+	cat >expect <<-EOF &&
	+	$(test_oid zero) $old_oid
	+	$old_oid $new_oid
	+	EOF
	+	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
	+	test_cmp expect actual &&
	+
	+	git -C repo stash drop stash@{1} &&
	+	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
	+	cat >expect <<-EOF &&
	+	$(test_oid zero) $new_oid
	+	EOF
	+	test_cmp expect actual
	+'
	+
	 test_expect_success 'stash pop' '
	 	git reset --hard &&
	 	git stash pop &&

I>e. the only reason you need to add that -C etc. is because you'd like
to set it up in a subdirectory. Easier to just add the tiny bity of
logic to make that a sub-shell instead.


^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v3 0/3] libify reflog
  2022-03-02 16:43       ` John Cai
@ 2022-03-02 18:55         ` Ævar Arnfjörð Bjarmason
  0 siblings, 0 replies; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-03-02 18:55 UTC (permalink / raw)
  To: John Cai; +Cc: git, John Cai via GitGitGadget, Junio C Hamano, Taylor Blau


On Wed, Mar 02 2022, John Cai wrote:

> Just wanted to bump this thread. It'd be good to get another ack on these
> last set of changes.

Hi. Sorry that I didn't look at it earlier.

This looks good to me and I think everything that's been brought up has
been addressed.

I left a nit on 1/3 suggesting a way to make that diff a bit smaller by
using a subshell instead of refactoring an existing function.

But I think with or without that & a ro-roll this would be good to
advance to "next" etc.

Thanks!


^ permalink raw reply	[flat|nested] 63+ messages in thread

* [PATCH v4 0/3] libify reflog
  2022-02-25 19:30   ` [PATCH v3 0/3] libify reflog John Cai via GitGitGadget
                       ` (3 preceding siblings ...)
  2022-02-25 19:38     ` [PATCH v3 0/3] libify reflog Taylor Blau
@ 2022-03-02 22:27     ` John Cai via GitGitGadget
  2022-03-02 22:27       ` [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
                         ` (3 more replies)
  4 siblings, 4 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-03-02 22:27 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai

In [1], there was a discussion around a bug report of stash not recovering
in the middle of the process when killed with ctl-c. It turned out to not be
a bug we need to fix. However, out of that discussion came the idea of
libifying reflog. This can stand alone as a code improvement.

stash.c currently shells out to call reflog to delete reflogs. Libify reflog
delete and call it from both builtin/reflog.c and builtin/stash.c.

This patch has three parts:

 * add missing test coverage for git stash delete
 * libify reflog's delete functionality and move some of the helpers into a
   reflog.c library and call reflog_delete from builtin/reflog.c
 * call reflog_delete from builtin/stash.c

Updates since v3:

 * refactored test to have a smaller diff

Updates since v2:

 * removed unnecessary includes
 * adjusted wrapping/whitespace in reflog.h
 * adjusted test to be isolated from other tests since currently tests for
   stash depend on each other. There was some discussion around this and
   even a possibility to refactor the tests. However, it would have been a
   larger effort than is worth for this series, so instead I just made one
   of the tests I added be isolated from the others.

Updates since v1:

 * added missing test coverage
 * squashed 1/3 and 2/3 together
 * moved enum into reflog.c
 * updated object.h's flag allocation mapping

 1. https://lore.kernel.org/git/220126.86h79qe692.gmgdl@evledraar.gmail.com/

John Cai (3):
  stash: add tests to ensure reflog --rewrite --updatref behavior
  reflog: libify delete reflog function and helpers
  stash: call reflog_delete() in reflog.c

 Makefile         |   1 +
 builtin/reflog.c | 455 +----------------------------------------------
 builtin/stash.c  |  18 +-
 object.h         |   2 +-
 reflog.c         | 432 ++++++++++++++++++++++++++++++++++++++++++++
 reflog.h         |  43 +++++
 t/t3903-stash.sh |  43 ++++-
 7 files changed, 527 insertions(+), 467 deletions(-)
 create mode 100644 reflog.c
 create mode 100644 reflog.h


base-commit: e6ebfd0e8cbbd10878070c8a356b5ad1b3ca464e
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1218%2Fjohn-cai%2Fjc-libify-reflog-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1218/john-cai/jc-libify-reflog-v4
Pull-Request: https://github.com/git/git/pull/1218

Range-diff vs v3:

 1:  33299825fc4 ! 1:  08bb8d3a9b9 stash: add tests to ensure reflog --rewrite --updatref behavior
     @@ t/t3903-stash.sh: diff_cmp () {
       }
       
      -test_expect_success 'stash some dirty working directory' '
     --	echo 1 >file &&
     --	git add file &&
     --	echo unrelated >other-file &&
     --	git add other-file &&
      +setup_stash() {
     -+	repo_dir=$1
     -+	if test -z $repo_dir; then
     -+		repo_dir="."
     -+	fi
     -+
     -+	echo 1 >$repo_dir/file &&
     -+	git -C $repo_dir add file &&
     -+	echo unrelated >$repo_dir/other-file &&
     -+	git -C $repo_dir add other-file &&
     - 	test_tick &&
     --	git commit -m initial &&
     --	echo 2 >file &&
     -+	git -C $repo_dir commit -m initial &&
     -+	echo 2 >$repo_dir/file &&
     + 	echo 1 >file &&
       	git add file &&
     --	echo 3 >file &&
     -+	echo 3 >$repo_dir/file &&
     - 	test_tick &&
     --	git stash &&
     --	git diff-files --quiet &&
     --	git diff-index --cached --quiet HEAD
     -+	git -C $repo_dir stash &&
     -+	git -C $repo_dir diff-files --quiet &&
     -+	git -C $repo_dir diff-index --cached --quiet HEAD
     + 	echo unrelated >other-file &&
     +@@ t/t3903-stash.sh: test_expect_success 'stash some dirty working directory' '
     + 	git stash &&
     + 	git diff-files --quiet &&
     + 	git diff-index --cached --quiet HEAD
      +}
      +
      +test_expect_success 'stash some dirty working directory' '
     @@ t/t3903-stash.sh: test_expect_success 'drop middle stash by index' '
      +
      +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
      +	git init repo &&
     -+	setup_stash repo &&
     ++	(
     ++		cd repo &&
     ++		setup_stash
     ++	) &&
      +	echo 9 >repo/file &&
      +
      +	old_oid="$(git -C repo rev-parse stash@{0})" &&
 2:  33adfee4ca6 = 2:  50471c2ee6f reflog: libify delete reflog function and helpers
 3:  b17d8e5d43a = 3:  cb32d9bfe60 stash: call reflog_delete() in reflog.c

-- 
gitgitgadget

^ permalink raw reply	[flat|nested] 63+ messages in thread

* [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-02 22:27     ` [PATCH v4 " John Cai via GitGitGadget
@ 2022-03-02 22:27       ` John Cai via GitGitGadget
  2022-03-02 23:32         ` Junio C Hamano
  2022-03-02 22:27       ` [PATCH v4 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
                         ` (2 subsequent siblings)
  3 siblings, 1 reply; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-03-02 22:27 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

There is missing test coverage to ensure that the resulting reflogs
after a git stash drop has had its old oid rewritten if applicable, and
if the refs/stash has been updated if applicable.

Add two tests that verify both of these happen.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
---
 t/t3903-stash.sh | 43 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 42 insertions(+), 1 deletion(-)

diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index b149e2af441..a2f8d0c52e7 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -41,7 +41,7 @@ diff_cmp () {
 	rm -f "$1.compare" "$2.compare"
 }
 
-test_expect_success 'stash some dirty working directory' '
+setup_stash() {
 	echo 1 >file &&
 	git add file &&
 	echo unrelated >other-file &&
@@ -55,6 +55,10 @@ test_expect_success 'stash some dirty working directory' '
 	git stash &&
 	git diff-files --quiet &&
 	git diff-index --cached --quiet HEAD
+}
+
+test_expect_success 'stash some dirty working directory' '
+	setup_stash
 '
 
 cat >expect <<EOF
@@ -185,6 +189,43 @@ test_expect_success 'drop middle stash by index' '
 	test 1 = $(git show HEAD:file)
 '
 
+test_expect_success 'drop stash reflog updates refs/stash' '
+	git reset --hard &&
+	git rev-parse refs/stash >expect &&
+	echo 9 >file &&
+	git stash &&
+	git stash drop stash@{0} &&
+	git rev-parse refs/stash >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
+	git init repo &&
+	(
+		cd repo &&
+		setup_stash
+	) &&
+	echo 9 >repo/file &&
+
+	old_oid="$(git -C repo rev-parse stash@{0})" &&
+	git -C repo stash &&
+	new_oid="$(git -C repo rev-parse stash@{0})" &&
+
+	cat >expect <<-EOF &&
+	$(test_oid zero) $old_oid
+	$old_oid $new_oid
+	EOF
+	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
+	test_cmp expect actual &&
+
+	git -C repo stash drop stash@{1} &&
+	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
+	cat >expect <<-EOF &&
+	$(test_oid zero) $new_oid
+	EOF
+	test_cmp expect actual
+'
+
 test_expect_success 'stash pop' '
 	git reset --hard &&
 	git stash pop &&
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 63+ messages in thread

* [PATCH v4 2/3] reflog: libify delete reflog function and helpers
  2022-03-02 22:27     ` [PATCH v4 " John Cai via GitGitGadget
  2022-03-02 22:27       ` [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
@ 2022-03-02 22:27       ` John Cai via GitGitGadget
  2022-03-02 22:27       ` [PATCH v4 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
  2022-03-02 23:34       ` [PATCH v4 0/3] libify reflog Junio C Hamano
  3 siblings, 0 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-03-02 22:27 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

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>
---
 Makefile         |   1 +
 builtin/reflog.c | 455 +----------------------------------------------
 object.h         |   2 +-
 reflog.c         | 432 ++++++++++++++++++++++++++++++++++++++++++++
 reflog.h         |  43 +++++
 5 files changed, 481 insertions(+), 452 deletions(-)
 create mode 100644 reflog.c
 create mode 100644 reflog.h

diff --git a/Makefile b/Makefile
index 6f0b4b775fe..876d4dfd6cb 100644
--- 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
diff --git a/builtin/reflog.c b/builtin/reflog.c
index 85b838720c3..940db196f62 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -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;
 }
 
diff --git a/object.h b/object.h
index cb556ab7753..a2219464c2b 100644
--- 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
index 00000000000..333fd8708fe
--- /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
index 00000000000..d2906fb9f8d
--- /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 */
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 63+ messages in thread

* [PATCH v4 3/3] stash: call reflog_delete() in reflog.c
  2022-03-02 22:27     ` [PATCH v4 " John Cai via GitGitGadget
  2022-03-02 22:27       ` [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
  2022-03-02 22:27       ` [PATCH v4 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
@ 2022-03-02 22:27       ` John Cai via GitGitGadget
  2022-03-02 23:34       ` [PATCH v4 0/3] libify reflog Junio C Hamano
  3 siblings, 0 replies; 63+ messages in thread
From: John Cai via GitGitGadget @ 2022-03-02 22:27 UTC (permalink / raw)
  To: git; +Cc: Ævar Arnfjörð Bjarmason, Taylor Blau, John Cai,
	John Cai

From: John Cai <johncai86@gmail.com>

Now that cmd_reflog_delete has been libified an exported it into a new
reflog.c library so we can call it directly from builtin/stash.c. This
not only gives us a performance gain since we don't need to create a
subprocess, but it also allows us to use the ref transactions api in the
future.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: John Cai <johncai86@gmail.com>
---
 builtin/stash.c | 18 ++++--------------
 1 file changed, 4 insertions(+), 14 deletions(-)

diff --git a/builtin/stash.c b/builtin/stash.c
index 5897febfbec..3e2f478b761 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -17,6 +17,7 @@
 #include "diffcore.h"
 #include "exec-cmd.h"
 #include "entry.h"
+#include "reflog.h"
 
 #define INCLUDE_ALL_FILES 2
 
@@ -634,20 +635,9 @@ static int reflog_is_empty(const char *refname)
 
 static int do_drop_stash(struct stash_info *info, int quiet)
 {
-	int ret;
-	struct child_process cp_reflog = CHILD_PROCESS_INIT;
-
-	/*
-	 * reflog does not provide a simple function for deleting refs. One will
-	 * need to be added to avoid implementing too much reflog code here
-	 */
-
-	cp_reflog.git_cmd = 1;
-	strvec_pushl(&cp_reflog.args, "reflog", "delete", "--updateref",
-		     "--rewrite", NULL);
-	strvec_push(&cp_reflog.args, info->revision.buf);
-	ret = run_command(&cp_reflog);
-	if (!ret) {
+	if (!reflog_delete(info->revision.buf,
+			   EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF,
+			   0)) {
 		if (!quiet)
 			printf_ln(_("Dropped %s (%s)"), info->revision.buf,
 				  oid_to_hex(&info->w_commit));
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-02 22:27       ` [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
@ 2022-03-02 23:32         ` Junio C Hamano
  2022-03-03 15:22           ` John Cai
  2022-03-03 16:11           ` Phillip Wood
  0 siblings, 2 replies; 63+ messages in thread
From: Junio C Hamano @ 2022-03-02 23:32 UTC (permalink / raw)
  To: John Cai via GitGitGadget
  Cc: git, Ævar Arnfjörð Bjarmason, Taylor Blau,
	John Cai

"John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: John Cai <johncai86@gmail.com>
>
> There is missing test coverage to ensure that the resulting reflogs
> after a git stash drop has had its old oid rewritten if applicable, and
> if the refs/stash has been updated if applicable.
>
> Add two tests that verify both of these happen.
>
> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
> Signed-off-by: John Cai <johncai86@gmail.com>
> ---
>  t/t3903-stash.sh | 43 ++++++++++++++++++++++++++++++++++++++++++-
>  1 file changed, 42 insertions(+), 1 deletion(-)
>
> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
> index b149e2af441..a2f8d0c52e7 100755
> --- a/t/t3903-stash.sh
> +++ b/t/t3903-stash.sh
> @@ -41,7 +41,7 @@ diff_cmp () {
>  	rm -f "$1.compare" "$2.compare"
>  }
>  
> -test_expect_success 'stash some dirty working directory' '
> +setup_stash() {

Style.

	setup_stash () {

but more importantly, is this really setting up "stash"?
"setup_stash_test" or something, perhaps?

> +test_expect_success 'stash some dirty working directory' '
> +	setup_stash
>  '

OK.

> +test_expect_success 'drop stash reflog updates refs/stash' '
> +	git reset --hard &&
> +	git rev-parse refs/stash >expect &&
> +	echo 9 >file &&
> +	git stash &&
> +	git stash drop stash@{0} &&
> +	git rev-parse refs/stash >actual &&
> +	test_cmp expect actual
> +'
> +
> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
> +	git init repo &&
> +	(
> +		cd repo &&
> +		setup_stash
> +	) &&

Hmph, so this is done inside the subdirectory.  The implementation
of the helper in this iteration does look cleaner than in the
previous iteration.

But these many references to "repo/" and "-C repo" we see below
makes me wonder why we do not put the whole thing inside the
subshell we started earlier.

i.e.

	git init repo &&
	(
		cd repo &&
		setup_stash_test &&

		echo 9 >file &&
		old=$(git rev-parse stash@{0}) &&
		git stash &&
		new=$(git rev-parse stash@{0}) &&
		...

		test_cmp expect actual
	)

> +	echo 9 >repo/file &&
> +
> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
> +	git -C repo stash &&
> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
> +
> +	cat >expect <<-EOF &&
> +	$(test_oid zero) $old_oid
> +	$old_oid $new_oid
> +	EOF
> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
> +	test_cmp expect actual &&
> +
> +	git -C repo stash drop stash@{1} &&
> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
> +	cat >expect <<-EOF &&
> +	$(test_oid zero) $new_oid
> +	EOF
> +	test_cmp expect actual
> +'
> +
>  test_expect_success 'stash pop' '
>  	git reset --hard &&
>  	git stash pop &&

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 0/3] libify reflog
  2022-03-02 22:27     ` [PATCH v4 " John Cai via GitGitGadget
                         ` (2 preceding siblings ...)
  2022-03-02 22:27       ` [PATCH v4 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
@ 2022-03-02 23:34       ` Junio C Hamano
  3 siblings, 0 replies; 63+ messages in thread
From: Junio C Hamano @ 2022-03-02 23:34 UTC (permalink / raw)
  To: John Cai via GitGitGadget
  Cc: git, Ævar Arnfjörð Bjarmason, Taylor Blau,
	John Cai

"John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:

> This patch has three parts:
>
>  * add missing test coverage for git stash delete

This looking better, relative to the previous iteration.

>  * libify reflog's delete functionality and move some of the helpers into a
>    reflog.c library and call reflog_delete from builtin/reflog.c

This part hasn't changed since the implementation in the previous
round I've looked at; looked sane.

>  * call reflog_delete from builtin/stash.c

Likewise.

Thanks.  Will queue.

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-02 23:32         ` Junio C Hamano
@ 2022-03-03 15:22           ` John Cai
  2022-03-03 16:11           ` Phillip Wood
  1 sibling, 0 replies; 63+ messages in thread
From: John Cai @ 2022-03-03 15:22 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: John Cai via GitGitGadget, git,
	Ævar Arnfjörð Bjarmason, Taylor Blau

Hi Junio,

On 2 Mar 2022, at 18:32, Junio C Hamano wrote:

> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
>> From: John Cai <johncai86@gmail.com>
>>
>> There is missing test coverage to ensure that the resulting reflogs
>> after a git stash drop has had its old oid rewritten if applicable, and
>> if the refs/stash has been updated if applicable.
>>
>> Add two tests that verify both of these happen.
>>
>> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
>> Signed-off-by: John Cai <johncai86@gmail.com>
>> ---
>>  t/t3903-stash.sh | 43 ++++++++++++++++++++++++++++++++++++++++++-
>>  1 file changed, 42 insertions(+), 1 deletion(-)
>>
>> diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
>> index b149e2af441..a2f8d0c52e7 100755
>> --- a/t/t3903-stash.sh
>> +++ b/t/t3903-stash.sh
>> @@ -41,7 +41,7 @@ diff_cmp () {
>>  	rm -f "$1.compare" "$2.compare"
>>  }
>>
>> -test_expect_success 'stash some dirty working directory' '
>> +setup_stash() {
>
> Style.
>
> 	setup_stash () {
>
> but more importantly, is this really setting up "stash"?
> "setup_stash_test" or something, perhaps?
>
>> +test_expect_success 'stash some dirty working directory' '
>> +	setup_stash
>>  '
>
> OK.
>
>> +test_expect_success 'drop stash reflog updates refs/stash' '
>> +	git reset --hard &&
>> +	git rev-parse refs/stash >expect &&
>> +	echo 9 >file &&
>> +	git stash &&
>> +	git stash drop stash@{0} &&
>> +	git rev-parse refs/stash >actual &&
>> +	test_cmp expect actual
>> +'
>> +
>> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
>> +	git init repo &&
>> +	(
>> +		cd repo &&
>> +		setup_stash
>> +	) &&
>
> Hmph, so this is done inside the subdirectory.  The implementation
> of the helper in this iteration does look cleaner than in the
> previous iteration.
>
> But these many references to "repo/" and "-C repo" we see below
> makes me wonder why we do not put the whole thing inside the
> subshell we started earlier.
>
> i.e.
>
> 	git init repo &&
> 	(
> 		cd repo &&
> 		setup_stash_test &&
>
> 		echo 9 >file &&
> 		old=$(git rev-parse stash@{0}) &&
> 		git stash &&
> 		new=$(git rev-parse stash@{0}) &&
> 		...
>
> 		test_cmp expect actual
> 	)
>

makes sense to me. Is this worth a re-roll and sending out another series to the list? or is it sufficient to make the change on my branch?

>> +	echo 9 >repo/file &&
>> +
>> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
>> +	git -C repo stash &&
>> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
>> +
>> +	cat >expect <<-EOF &&
>> +	$(test_oid zero) $old_oid
>> +	$old_oid $new_oid
>> +	EOF
>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>> +	test_cmp expect actual &&
>> +
>> +	git -C repo stash drop stash@{1} &&
>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>> +	cat >expect <<-EOF &&
>> +	$(test_oid zero) $new_oid
>> +	EOF
>> +	test_cmp expect actual
>> +'
>> +
>>  test_expect_success 'stash pop' '
>>  	git reset --hard &&
>>  	git stash pop &&

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-02 23:32         ` Junio C Hamano
  2022-03-03 15:22           ` John Cai
@ 2022-03-03 16:11           ` Phillip Wood
  2022-03-03 16:52             ` Ævar Arnfjörð Bjarmason
  1 sibling, 1 reply; 63+ messages in thread
From: Phillip Wood @ 2022-03-03 16:11 UTC (permalink / raw)
  To: Junio C Hamano, John Cai via GitGitGadget
  Cc: git, Ævar Arnfjörð Bjarmason, Taylor Blau,
	John Cai

On 02/03/2022 23:32, Junio C Hamano wrote:
> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
> [...]
>> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
>> +	git init repo &&
>> +	(
>> +		cd repo &&
>> +		setup_stash
>> +	) &&
> 
> Hmph, so this is done inside the subdirectory.  The implementation
> of the helper in this iteration does look cleaner than in the
> previous iteration.
> 
> But these many references to "repo/" and "-C repo" we see below
> makes me wonder why we do not put the whole thing inside the
> subshell we started earlier.
> 
> i.e.
> 
> 	git init repo &&
> 	(
> 		cd repo &&
> 		setup_stash_test &&
> 
> 		echo 9 >file &&
> 		old=$(git rev-parse stash@{0}) &&
> 		git stash &&
> 		new=$(git rev-parse stash@{0}) &&
> 		...
> 
> 		test_cmp expect actual
> 	)
>

I wonder if we could avoid the subshell entirely and avoid relying on 
REFFILES (assuming we're not trying to test the implementation details 
of that refs backend) by doing something like

test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
	old=$(git rev-parse stash@{0}) &&
	setup_stash_test &&
	git rev-list -g stash >tmp &&
	sed /$old/d tmp >expect &&
	git rev-list -g stash >actual &&
	test_cmp expect actual
'

Best Wishes

Phillip

>> +	echo 9 >repo/file &&
>> +
>> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
>> +	git -C repo stash &&
>> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
>> +
>> +	cat >expect <<-EOF &&
>> +	$(test_oid zero) $old_oid
>> +	$old_oid $new_oid
>> +	EOF
>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>> +	test_cmp expect actual &&
>> +
>> +	git -C repo stash drop stash@{1} &&
>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>> +	cat >expect <<-EOF &&
>> +	$(test_oid zero) $new_oid
>> +	EOF
>> +	test_cmp expect actual
>> +'
>> +
>>   test_expect_success 'stash pop' '
>>   	git reset --hard &&
>>   	git stash pop &&

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-03 16:11           ` Phillip Wood
@ 2022-03-03 16:52             ` Ævar Arnfjörð Bjarmason
  2022-03-03 17:28               ` Phillip Wood
  0 siblings, 1 reply; 63+ messages in thread
From: Ævar Arnfjörð Bjarmason @ 2022-03-03 16:52 UTC (permalink / raw)
  To: phillip.wood
  Cc: Junio C Hamano, John Cai via GitGitGadget, git, Taylor Blau,
	John Cai


On Thu, Mar 03 2022, Phillip Wood wrote:

> On 02/03/2022 23:32, Junio C Hamano wrote:
>> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
>> [...]
>>> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
>>> +	git init repo &&
>>> +	(
>>> +		cd repo &&
>>> +		setup_stash
>>> +	) &&
>> Hmph, so this is done inside the subdirectory.  The implementation
>> of the helper in this iteration does look cleaner than in the
>> previous iteration.
>> But these many references to "repo/" and "-C repo" we see below
>> makes me wonder why we do not put the whole thing inside the
>> subshell we started earlier.
>> i.e.
>> 	git init repo &&
>> 	(
>> 		cd repo &&
>> 		setup_stash_test &&
>> 		echo 9 >file &&
>> 		old=$(git rev-parse stash@{0}) &&
>> 		git stash &&
>> 		new=$(git rev-parse stash@{0}) &&
>> 		...
>> 		test_cmp expect actual
>> 	)
>>
>
> I wonder if we could avoid the subshell entirely and avoid relying on
> REFFILES (assuming we're not trying to test the implementation details 
> of that refs backend) by doing something like
>
> test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
> 	old=$(git rev-parse stash@{0}) &&
> 	setup_stash_test &&
> 	git rev-list -g stash >tmp &&
> 	sed /$old/d tmp >expect &&
> 	git rev-list -g stash >actual &&
> 	test_cmp expect actual
> '

Unless I'm missing something that "rev-list -g" will emit only the RHS
of the stash logs, i.e. no "0000..." etc.

And if we only look at that the difference with specifying the flag
isn't visible, no?

>>> +	echo 9 >repo/file &&
>>> +
>>> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
>>> +	git -C repo stash &&
>>> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
>>> +
>>> +	cat >expect <<-EOF &&
>>> +	$(test_oid zero) $old_oid
>>> +	$old_oid $new_oid
>>> +	EOF
>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>> +	test_cmp expect actual &&
>>> +
>>> +	git -C repo stash drop stash@{1} &&
>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>> +	cat >expect <<-EOF &&
>>> +	$(test_oid zero) $new_oid
>>> +	EOF
>>> +	test_cmp expect actual
>>> +'
>>> +
>>>   test_expect_success 'stash pop' '
>>>   	git reset --hard &&
>>>   	git stash pop &&


^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-03 16:52             ` Ævar Arnfjörð Bjarmason
@ 2022-03-03 17:28               ` Phillip Wood
  2022-03-03 19:12                 ` John Cai
  0 siblings, 1 reply; 63+ messages in thread
From: Phillip Wood @ 2022-03-03 17:28 UTC (permalink / raw)
  To: Ævar Arnfjörð Bjarmason, phillip.wood
  Cc: Junio C Hamano, John Cai via GitGitGadget, git, Taylor Blau,
	John Cai

On 03/03/2022 16:52, Ævar Arnfjörð Bjarmason wrote:
> 
> On Thu, Mar 03 2022, Phillip Wood wrote:
> 
>> On 02/03/2022 23:32, Junio C Hamano wrote:
>>> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>> [...]
>>>> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
>>>> +	git init repo &&
>>>> +	(
>>>> +		cd repo &&
>>>> +		setup_stash
>>>> +	) &&
>>> Hmph, so this is done inside the subdirectory.  The implementation
>>> of the helper in this iteration does look cleaner than in the
>>> previous iteration.
>>> But these many references to "repo/" and "-C repo" we see below
>>> makes me wonder why we do not put the whole thing inside the
>>> subshell we started earlier.
>>> i.e.
>>> 	git init repo &&
>>> 	(
>>> 		cd repo &&
>>> 		setup_stash_test &&
>>> 		echo 9 >file &&
>>> 		old=$(git rev-parse stash@{0}) &&
>>> 		git stash &&
>>> 		new=$(git rev-parse stash@{0}) &&
>>> 		...
>>> 		test_cmp expect actual
>>> 	)
>>>
>>
>> I wonder if we could avoid the subshell entirely and avoid relying on
>> REFFILES (assuming we're not trying to test the implementation details
>> of that refs backend) by doing something like
>>
>> test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>> 	old=$(git rev-parse stash@{0}) &&
>> 	setup_stash_test &&
>> 	git rev-list -g stash >tmp &&
>> 	sed /$old/d tmp >expect &&
>> 	git rev-list -g stash >actual &&
>> 	test_cmp expect actual
>> '
> 
> Unless I'm missing something that "rev-list -g" will emit only the RHS
> of the stash logs, i.e. no "0000..." etc.
> 
> And if we only look at that the difference with specifying the flag
> isn't visible, no?

Maybe I'm missing what this test is actually needs to do. I thought it 
just needed to check that the deleted stash is removed from the reflog 
and the others are unchanged. You're right that it wont show the LHS and 
if that is important then you need to read the log file directly.

Best Wishes

Phillip


>>>> +	echo 9 >repo/file &&
>>>> +
>>>> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
>>>> +	git -C repo stash &&
>>>> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
>>>> +
>>>> +	cat >expect <<-EOF &&
>>>> +	$(test_oid zero) $old_oid
>>>> +	$old_oid $new_oid
>>>> +	EOF
>>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>>> +	test_cmp expect actual &&
>>>> +
>>>> +	git -C repo stash drop stash@{1} &&
>>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>>> +	cat >expect <<-EOF &&
>>>> +	$(test_oid zero) $new_oid
>>>> +	EOF
>>>> +	test_cmp expect actual
>>>> +'
>>>> +
>>>>    test_expect_success 'stash pop' '
>>>>    	git reset --hard &&
>>>>    	git stash pop &&
> 


^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-03 17:28               ` Phillip Wood
@ 2022-03-03 19:12                 ` John Cai
  2022-03-08 10:39                   ` Phillip Wood
  0 siblings, 1 reply; 63+ messages in thread
From: John Cai @ 2022-03-03 19:12 UTC (permalink / raw)
  To: phillip.wood
  Cc: Ævar Arnfjörð Bjarmason, Junio C Hamano,
	John Cai via GitGitGadget, git, Taylor Blau

Hi Phillip,

On 3 Mar 2022, at 12:28, Phillip Wood wrote:

> On 03/03/2022 16:52, Ævar Arnfjörð Bjarmason wrote:
>>
>> On Thu, Mar 03 2022, Phillip Wood wrote:
>>
>>> On 02/03/2022 23:32, Junio C Hamano wrote:
>>>> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>>> [...]
>>>>> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
>>>>> +	git init repo &&
>>>>> +	(
>>>>> +		cd repo &&
>>>>> +		setup_stash
>>>>> +	) &&
>>>> Hmph, so this is done inside the subdirectory.  The implementation
>>>> of the helper in this iteration does look cleaner than in the
>>>> previous iteration.
>>>> But these many references to "repo/" and "-C repo" we see below
>>>> makes me wonder why we do not put the whole thing inside the
>>>> subshell we started earlier.
>>>> i.e.
>>>> 	git init repo &&
>>>> 	(
>>>> 		cd repo &&
>>>> 		setup_stash_test &&
>>>> 		echo 9 >file &&
>>>> 		old=$(git rev-parse stash@{0}) &&
>>>> 		git stash &&
>>>> 		new=$(git rev-parse stash@{0}) &&
>>>> 		...
>>>> 		test_cmp expect actual
>>>> 	)
>>>>
>>>
>>> I wonder if we could avoid the subshell entirely and avoid relying on
>>> REFFILES (assuming we're not trying to test the implementation details
>>> of that refs backend) by doing something like
>>>
>>> test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>> 	old=$(git rev-parse stash@{0}) &&
>>> 	setup_stash_test &&
>>> 	git rev-list -g stash >tmp &&
>>> 	sed /$old/d tmp >expect &&
>>> 	git rev-list -g stash >actual &&
>>> 	test_cmp expect actual
>>> '
>>
>> Unless I'm missing something that "rev-list -g" will emit only the RHS
>> of the stash logs, i.e. no "0000..." etc.
>>
>> And if we only look at that the difference with specifying the flag
>> isn't visible, no?
>
> Maybe I'm missing what this test is actually needs to do. I thought it just needed to check that the deleted stash is removed from the reflog and the others are unchanged. You're right that it wont show the LHS and if that is important then you need to read the log file directly.

We had discussed this briefly in [1], but the --rewrite option for reflog delete will rewrite the LHS, which is not visible to normal ref API users. So the only way to test that this happened is to reach inside of the file.

1. https://lore.kernel.org/git/xmqqczjdp2g8.fsf@gitster.g/
>
> Best Wishes
>
> Phillip

thanks,
John

>
>
>>>>> +	echo 9 >repo/file &&
>>>>> +
>>>>> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
>>>>> +	git -C repo stash &&
>>>>> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
>>>>> +
>>>>> +	cat >expect <<-EOF &&
>>>>> +	$(test_oid zero) $old_oid
>>>>> +	$old_oid $new_oid
>>>>> +	EOF
>>>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>>>> +	test_cmp expect actual &&
>>>>> +
>>>>> +	git -C repo stash drop stash@{1} &&
>>>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>>>> +	cat >expect <<-EOF &&
>>>>> +	$(test_oid zero) $new_oid
>>>>> +	EOF
>>>>> +	test_cmp expect actual
>>>>> +'
>>>>> +
>>>>>    test_expect_success 'stash pop' '
>>>>>    	git reset --hard &&
>>>>>    	git stash pop &&
>>

^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-03 19:12                 ` John Cai
@ 2022-03-08 10:39                   ` Phillip Wood
  2022-03-08 18:09                     ` John Cai
  0 siblings, 1 reply; 63+ messages in thread
From: Phillip Wood @ 2022-03-08 10:39 UTC (permalink / raw)
  To: John Cai, phillip.wood
  Cc: Ævar Arnfjörð Bjarmason, Junio C Hamano,
	John Cai via GitGitGadget, git, Taylor Blau

Hi John

On 03/03/2022 19:12, John Cai wrote:
> Hi Phillip,
> 
> On 3 Mar 2022, at 12:28, Phillip Wood wrote:
> 
>> On 03/03/2022 16:52, Ævar Arnfjörð Bjarmason wrote:
>>>
>>> On Thu, Mar 03 2022, Phillip Wood wrote:
>>>
>>>> On 02/03/2022 23:32, Junio C Hamano wrote:
>>>>> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>>>> [...]
>>>>>> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
>>>>>> +	git init repo &&
>>>>>> +	(
>>>>>> +		cd repo &&
>>>>>> +		setup_stash
>>>>>> +	) &&
>>>>> Hmph, so this is done inside the subdirectory.  The implementation
>>>>> of the helper in this iteration does look cleaner than in the
>>>>> previous iteration.
>>>>> But these many references to "repo/" and "-C repo" we see below
>>>>> makes me wonder why we do not put the whole thing inside the
>>>>> subshell we started earlier.
>>>>> i.e.
>>>>> 	git init repo &&
>>>>> 	(
>>>>> 		cd repo &&
>>>>> 		setup_stash_test &&
>>>>> 		echo 9 >file &&
>>>>> 		old=$(git rev-parse stash@{0}) &&
>>>>> 		git stash &&
>>>>> 		new=$(git rev-parse stash@{0}) &&
>>>>> 		...
>>>>> 		test_cmp expect actual
>>>>> 	)
>>>>>
>>>>
>>>> I wonder if we could avoid the subshell entirely and avoid relying on
>>>> REFFILES (assuming we're not trying to test the implementation details
>>>> of that refs backend) by doing something like
>>>>
>>>> test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>>> 	old=$(git rev-parse stash@{0}) &&
>>>> 	setup_stash_test &&
>>>> 	git rev-list -g stash >tmp &&
>>>> 	sed /$old/d tmp >expect &&
>>>> 	git rev-list -g stash >actual &&
>>>> 	test_cmp expect actual
>>>> '
>>>
>>> Unless I'm missing something that "rev-list -g" will emit only the RHS
>>> of the stash logs, i.e. no "0000..." etc.
>>>
>>> And if we only look at that the difference with specifying the flag
>>> isn't visible, no?
>>
>> Maybe I'm missing what this test is actually needs to do. I thought it just needed to check that the deleted stash is removed from the reflog and the others are unchanged. You're right that it wont show the LHS and if that is important then you need to read the log file directly.
> 
> We had discussed this briefly in [1], but the --rewrite option for reflog delete will rewrite the LHS, which is not visible to normal ref API users. So the only way to test that this happened is to reach inside of the file.
> 
> 1. https://lore.kernel.org/git/xmqqczjdp2g8.fsf@gitster.g/

Thanks for the pointer, that was useful context that could maybe be 
added to the commit message to explain why the test needs to check the 
lhs of the reflog if you reroll.

Best Wishes

Phillip

>>
>> Best Wishes
>>
>> Phillip
> 
> thanks,
> John
> 
>>
>>
>>>>>> +	echo 9 >repo/file &&
>>>>>> +
>>>>>> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
>>>>>> +	git -C repo stash &&
>>>>>> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
>>>>>> +
>>>>>> +	cat >expect <<-EOF &&
>>>>>> +	$(test_oid zero) $old_oid
>>>>>> +	$old_oid $new_oid
>>>>>> +	EOF
>>>>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>>>>> +	test_cmp expect actual &&
>>>>>> +
>>>>>> +	git -C repo stash drop stash@{1} &&
>>>>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>>>>> +	cat >expect <<-EOF &&
>>>>>> +	$(test_oid zero) $new_oid
>>>>>> +	EOF
>>>>>> +	test_cmp expect actual
>>>>>> +'
>>>>>> +
>>>>>>     test_expect_success 'stash pop' '
>>>>>>     	git reset --hard &&
>>>>>>     	git stash pop &&
>>>


^ permalink raw reply	[flat|nested] 63+ messages in thread

* Re: [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior
  2022-03-08 10:39                   ` Phillip Wood
@ 2022-03-08 18:09                     ` John Cai
  0 siblings, 0 replies; 63+ messages in thread
From: John Cai @ 2022-03-08 18:09 UTC (permalink / raw)
  To: phillip.wood
  Cc: Ævar Arnfjörð Bjarmason, Junio C Hamano,
	John Cai via GitGitGadget, git, Taylor Blau

Hi Phillip,

On 8 Mar 2022, at 5:39, Phillip Wood wrote:

> Hi John
>
> On 03/03/2022 19:12, John Cai wrote:
>> Hi Phillip,
>>
>> On 3 Mar 2022, at 12:28, Phillip Wood wrote:
>>
>>> On 03/03/2022 16:52, Ævar Arnfjörð Bjarmason wrote:
>>>>
>>>> On Thu, Mar 03 2022, Phillip Wood wrote:
>>>>
>>>>> On 02/03/2022 23:32, Junio C Hamano wrote:
>>>>>> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes:
>>>>>> [...]
>>>>>>> +test_expect_success REFFILES 'drop stash reflog updates refs/stash with rewrite' '
>>>>>>> +	git init repo &&
>>>>>>> +	(
>>>>>>> +		cd repo &&
>>>>>>> +		setup_stash
>>>>>>> +	) &&
>>>>>> Hmph, so this is done inside the subdirectory.  The implementation
>>>>>> of the helper in this iteration does look cleaner than in the
>>>>>> previous iteration.
>>>>>> But these many references to "repo/" and "-C repo" we see below
>>>>>> makes me wonder why we do not put the whole thing inside the
>>>>>> subshell we started earlier.
>>>>>> i.e.
>>>>>> 	git init repo &&
>>>>>> 	(
>>>>>> 		cd repo &&
>>>>>> 		setup_stash_test &&
>>>>>> 		echo 9 >file &&
>>>>>> 		old=$(git rev-parse stash@{0}) &&
>>>>>> 		git stash &&
>>>>>> 		new=$(git rev-parse stash@{0}) &&
>>>>>> 		...
>>>>>> 		test_cmp expect actual
>>>>>> 	)
>>>>>>
>>>>>
>>>>> I wonder if we could avoid the subshell entirely and avoid relying on
>>>>> REFFILES (assuming we're not trying to test the implementation details
>>>>> of that refs backend) by doing something like
>>>>>
>>>>> test_expect_success 'drop stash reflog updates refs/stash with rewrite' '
>>>>> 	old=$(git rev-parse stash@{0}) &&
>>>>> 	setup_stash_test &&
>>>>> 	git rev-list -g stash >tmp &&
>>>>> 	sed /$old/d tmp >expect &&
>>>>> 	git rev-list -g stash >actual &&
>>>>> 	test_cmp expect actual
>>>>> '
>>>>
>>>> Unless I'm missing something that "rev-list -g" will emit only the RHS
>>>> of the stash logs, i.e. no "0000..." etc.
>>>>
>>>> And if we only look at that the difference with specifying the flag
>>>> isn't visible, no?
>>>
>>> Maybe I'm missing what this test is actually needs to do. I thought it just needed to check that the deleted stash is removed from the reflog and the others are unchanged. You're right that it wont show the LHS and if that is important then you need to read the log file directly.
>>
>> We had discussed this briefly in [1], but the --rewrite option for reflog delete will rewrite the LHS, which is not visible to normal ref API users. So the only way to test that this happened is to reach inside of the file.
>>
>> 1. https://lore.kernel.org/git/xmqqczjdp2g8.fsf@gitster.g/
>
> Thanks for the pointer, that was useful context that could maybe be added to the commit message to explain why the test needs to check the lhs of the reflog if you reroll.

Good point. that would be helpful context since it took me a while to figure it out myself--will re-roll, thanks!

>
> Best Wishes
>
> Phillip
>
>>>
>>> Best Wishes
>>>
>>> Phillip
>>
>> thanks,
>> John
>>
>>>
>>>
>>>>>>> +	echo 9 >repo/file &&
>>>>>>> +
>>>>>>> +	old_oid="$(git -C repo rev-parse stash@{0})" &&
>>>>>>> +	git -C repo stash &&
>>>>>>> +	new_oid="$(git -C repo rev-parse stash@{0})" &&
>>>>>>> +
>>>>>>> +	cat >expect <<-EOF &&
>>>>>>> +	$(test_oid zero) $old_oid
>>>>>>> +	$old_oid $new_oid
>>>>>>> +	EOF
>>>>>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>>>>>> +	test_cmp expect actual &&
>>>>>>> +
>>>>>>> +	git -C repo stash drop stash@{1} &&
>>>>>>> +	cut -d" " -f1-2 repo/.git/logs/refs/stash >actual &&
>>>>>>> +	cat >expect <<-EOF &&
>>>>>>> +	$(test_oid zero) $new_oid
>>>>>>> +	EOF
>>>>>>> +	test_cmp expect actual
>>>>>>> +'
>>>>>>> +
>>>>>>>     test_expect_success 'stash pop' '
>>>>>>>     	git reset --hard &&
>>>>>>>     	git stash pop &&
>>>>

^ permalink raw reply	[flat|nested] 63+ messages in thread

end of thread, other threads:[~2022-03-08 18:10 UTC | newest]

Thread overview: 63+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-02-18 18:40 [PATCH 0/3] libify reflog John Cai via GitGitGadget
2022-02-18 18:40 ` [PATCH 1/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
2022-02-18 19:10   ` Ævar Arnfjörð Bjarmason
2022-02-18 19:39     ` Taylor Blau
2022-02-18 19:48       ` Ævar Arnfjörð Bjarmason
2022-02-18 19:35   ` Taylor Blau
2022-02-21  1:43     ` John Cai
2022-02-21  1:50       ` Taylor Blau
2022-02-23 19:50         ` John Cai
2022-02-18 20:00   ` Junio C Hamano
2022-02-19  2:53     ` Ævar Arnfjörð Bjarmason
2022-02-19  3:02       ` Taylor Blau
2022-02-20  7:49       ` Junio C Hamano
2022-02-18 20:21   ` Junio C Hamano
2022-02-18 18:40 ` [PATCH 2/3] reflog: call reflog_delete from reflog.c John Cai via GitGitGadget
2022-02-18 19:15   ` Ævar Arnfjörð Bjarmason
2022-02-18 20:26     ` Junio C Hamano
2022-02-18 18:40 ` [PATCH 3/3] stash: " John Cai via GitGitGadget
2022-02-18 19:20   ` Ævar Arnfjörð Bjarmason
2022-02-19  0:21     ` Taylor Blau
2022-02-22  2:36     ` John Cai
2022-02-22 10:51       ` Ævar Arnfjörð Bjarmason
2022-02-18 19:29 ` [PATCH 0/3] libify reflog Ævar Arnfjörð Bjarmason
2022-02-22 18:30 ` [PATCH v2 " John Cai via GitGitGadget
2022-02-22 18:30   ` [PATCH v2 1/3] stash: add test to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
2022-02-23  8:54     ` Ævar Arnfjörð Bjarmason
2022-02-23 21:27       ` Junio C Hamano
2022-02-23 21:50         ` Ævar Arnfjörð Bjarmason
2022-02-24 18:21           ` John Cai
2022-02-25 11:45             ` Ævar Arnfjörð Bjarmason
2022-02-25 17:23               ` Junio C Hamano
2022-02-23 21:50         ` John Cai
2022-02-23 22:51       ` Junio C Hamano
2022-02-23 23:12         ` John Cai
2022-02-23 23:27           ` Ævar Arnfjörð Bjarmason
2022-02-23 23:50           ` Junio C Hamano
2022-02-24 14:53             ` John Cai
2022-02-22 18:30   ` [PATCH v2 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
2022-02-23  9:02     ` Ævar Arnfjörð Bjarmason
2022-02-23 18:40       ` John Cai
2022-02-23 21:28     ` Junio C Hamano
2022-02-22 18:30   ` [PATCH v2 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
2022-02-25 19:30   ` [PATCH v3 0/3] libify reflog John Cai via GitGitGadget
2022-02-25 19:30     ` [PATCH v3 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
2022-03-02 18:52       ` Ævar Arnfjörð Bjarmason
2022-02-25 19:30     ` [PATCH v3 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
2022-02-25 19:30     ` [PATCH v3 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
2022-02-25 19:38     ` [PATCH v3 0/3] libify reflog Taylor Blau
2022-03-02 16:43       ` John Cai
2022-03-02 18:55         ` Ævar Arnfjörð Bjarmason
2022-03-02 22:27     ` [PATCH v4 " John Cai via GitGitGadget
2022-03-02 22:27       ` [PATCH v4 1/3] stash: add tests to ensure reflog --rewrite --updatref behavior John Cai via GitGitGadget
2022-03-02 23:32         ` Junio C Hamano
2022-03-03 15:22           ` John Cai
2022-03-03 16:11           ` Phillip Wood
2022-03-03 16:52             ` Ævar Arnfjörð Bjarmason
2022-03-03 17:28               ` Phillip Wood
2022-03-03 19:12                 ` John Cai
2022-03-08 10:39                   ` Phillip Wood
2022-03-08 18:09                     ` John Cai
2022-03-02 22:27       ` [PATCH v4 2/3] reflog: libify delete reflog function and helpers John Cai via GitGitGadget
2022-03-02 22:27       ` [PATCH v4 3/3] stash: call reflog_delete() in reflog.c John Cai via GitGitGadget
2022-03-02 23:34       ` [PATCH v4 0/3] libify reflog Junio C Hamano

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).