Skip to contentSkip to conversation

prune: mark rebase autostash and orig-head as reachable

PATCH Superseded Phillip Wood via GitGitGadget 5 participants 8 messages Feb 8, 2024 – May 14, 2024

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 prune: mark rebase autostash and orig-head as reachable

From: Phillip Wood <[email protected]>

Rebase records the oid of HEAD before rebasing and the commit created by "--autostash" in files in the rebase state directory. This means that the autostash commit is never reachable from any ref or reflog and when rebasing a detached HEAD the original HEAD can become unreachable if the user expires HEAD's the reflog while the rebase is running. Fix this by reading the relevant files when marking reachable commits.

Note that it is possible for the commit recorded in .git/rebase-merge/amend to be unreachable but pruning that object does not affect the operation of "git rebase --continue" as we're only interested in the object id, not in the object itself.

---
prune: mark rebase autostash and orig-head as reachable

Thanks for reporting this Orgad, it reminded me that I've been meaning to clean up this patch for a while.

I've got a patch on top of this that marks selected pseudorefs such as ORIG_HEAD and AUTO_MERGE as reachable but I've held off sending that as I think it would be better to rebase it on kn/for-all-refs once that has stabilized as that will hopefully allows us to mark all pseudorefs as reachable rather than using a hard-coded list.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1656%2Fphillipwood%2Fprune-protect-rebase-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1656/phillipwood/prune-protect-rebase-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1656

 reachable.c                 | 51 +++++++++++++++++++++++++++++++++++++
 t/t3407-rebase-abort.sh     | 17 ++++++++++++-
 t/t3420-rebase-autostash.sh | 10 ++++++++
 3 files changed, 77 insertions(+), 1 deletion(-)
reachable.c +51 -0
index f29b06a5d05..fef29422d4a 100644
@@ -17,6 +17,7 @@
 #include "pack-mtimes.h"
 #include "config.h"
 #include "run-command.h"
+#include "sequencer.h"
 
 struct connectivity_progress {
 	struct progress *progress;
@@ -30,6 +31,53 @@ static void update_progress(struct connectivity_progress *cp)
 		display_progress(cp->progress, cp->count);
 }
 
+static int add_one_file(const char *path, struct rev_info *revs)
+{
+	struct strbuf buf = STRBUF_INIT;
+	struct object_id oid;
+	struct object *object;
+
+	if (!read_oneliner(&buf, path, READ_ONELINER_SKIP_IF_EMPTY)) {
+		strbuf_release(&buf);
+		return 0;
+	}
+	strbuf_trim(&buf);
+	if (!get_oid_hex(buf.buf, &oid)) {
+		object = parse_object_or_die(&oid, buf.buf);
+		add_pending_object(revs, object, "");
+	}
+	return 0;
+}
+
+/* Mark objects recored in rebase state files as reachable. */
+static int add_rebase_files(struct rev_info *revs)
+{
+	struct strbuf buf = STRBUF_INIT;
+	size_t len;
+	const char *path[] = {
+		"rebase-apply/autostash",
+		"rebase-apply/orig-head",
+		"rebase-merge/autostash",
+		"rebase-merge/orig-head",
+	};
+	struct worktree **worktrees = get_worktrees();
+
+	for (struct worktree **wt = worktrees; *wt; wt++) {
+		strbuf_reset(&buf);
+		strbuf_addstr(&buf, get_worktree_git_dir(*wt));
+		strbuf_complete(&buf, '/');
+		len = buf.len;
+		for (size_t i = 0; i < ARRAY_SIZE(path); i++) {
+			strbuf_setlen(&buf, len);
+			strbuf_addstr(&buf, path[i]);
+			add_one_file(buf.buf, revs);
+		}
+	}
+	strbuf_release(&buf);
+	free_worktrees(worktrees);
+	return 0;
+}
+
 static int add_one_ref(const char *path, const struct object_id *oid,
 		       int flag, void *cb_data)
 {
@@ -322,6 +370,9 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog,
 	head_ref(add_one_ref, revs);
 	other_head_refs(add_one_ref, revs);
 
+	/* rebase autostash and orig-head */
+	add_rebase_files(revs);
+
 	/* Add all reflog info */
 	if (mark_reflog)
 		add_reflogs_to_pending(revs, 0);
t/t3407-rebase-abort.sh +16 -1
index ebbaed147a6..9f49c4228b6 100755
@@ -40,9 +40,24 @@ testrebase() {
 		test_path_is_missing "$state_dir"
 	'
 
+	test_expect_success "pre rebase$type head is marked as reachable" '
+		# Clean up the state from the previous one
+		git checkout -f --detach pre-rebase &&
+		test_tick &&
+		git commit --amend --only -m "reworded" &&
+		orig_head=$(git rev-parse HEAD) &&
+		test_must_fail git rebase$type main &&
+		# Stop ORIG_HEAD marking $state_dir/orig-head as reachable
+		git update-ref -d ORIG_HEAD &&
+		git reflog expire --expire="$GIT_COMMITTER_DATE" --all &&
+		git prune --expire=now &&
+		git rebase --abort &&
+		test_cmp_rev $orig_head HEAD
+	'
+
 	test_expect_success "rebase$type --abort after --skip" '
 		# Clean up the state from the previous one
-		git reset --hard pre-rebase &&
+		git checkout -B to-rebase pre-rebase &&
 		test_must_fail git rebase$type main &&
 		test_path_is_dir "$state_dir" &&
 		test_must_fail git rebase --skip &&
t/t3420-rebase-autostash.sh +10 -0
index 693934ee8be..1a820f14815 100755
@@ -333,4 +333,14 @@ test_expect_success 'never change active branch' '
 	test_cmp_rev not-the-feature-branch unrelated-onto-branch
 '
 
+test_expect_success 'autostash commit is marked as reachable' '
+	echo changed >file0 &&
+	git rebase --autostash --exec "git prune --expire=now" \
+		feature-branch^ feature-branch &&
+	# git rebase succeeds if the stash cannot be applied so we need to check
+	# the contents of file0
+	echo changed >expect &&
+	test_cmp expect file0
+'
+
 test_done

base-commit: 2a540e432fe5dff3cfa9d3bf7ca56db2ad12ebb9

Signature · 1 line
gitgitgadget
#1 lore raw
Quoted 22 lines · Phillip Wood via GitGitGadget

Rebase records the oid of HEAD before rebasing and the commit created by "--autostash" in files in the rebase state directory. This means that the autostash commit is never reachable from any ref or reflog and when rebasing a detached HEAD the original HEAD can become unreachable if the user expires HEAD's the reflog while the rebase is running. Fix this by reading the relevant files when marking reachable commits.

Note that it is possible for the commit recorded in .git/rebase-merge/amend to be unreachable but pruning that object does not affect the operation of "git rebase --continue" as we're only interested in the object id, not in the object itself.

---

reachable.c +5 -0
@@ -30,6 +31,53 @@ static void update_progress(struct connectivity_progress *cp)
+static int add_one_file(const char *path, struct rev_info *revs)
+{
+       struct strbuf buf = STRBUF_INIT;
+
+       if (!read_oneliner(&buf, path, READ_ONELINER_SKIP_IF_EMPTY)) {

reachable.c static void update_progress(struct connectivity_progress *cp) · line 45

Quoted 10 lines
reachable.c +10 -0
+               strbuf_release(&buf);
+               return 0;
+       }
+       strbuf_trim(&buf);
+       if (!get_oid_hex(buf.buf, &oid)) {
+               object = parse_object_or_die(&oid, buf.buf);
+               add_pending_object(revs, object, "");
+       }
+       return 0;
+}

Is this leaking the strbuf? Should the function instead end with:

    strbuf_release(&buf);
    return 0;

Also, what is the significance of the return value of this function? All code paths seem to be returning 0 unconditionally, and the caller ignores the return value.

reachable.c static void update_progress(struct connectivity_progress *cp) · line 52

Quoted 1 line
reachable.c +1 -0
+/* Mark objects recored in rebase state files as reachable. */

s/recored/recorded/

#2 lore raw

Hi Eric

Quoted 20 lines · Eric Sunshine

On Thu, Feb 8, 2024 at 12:00 PM Phillip Wood via GitGitGadget

Quoted 5 lines
reachable.c +5 -0
+static int add_one_file(const char *path, struct rev_info *revs)
+{
+       struct strbuf buf = STRBUF_INIT;
+
+       if (!read_oneliner(&buf, path, READ_ONELINER_SKIP_IF_EMPTY)) {

reachable.c static void update_progress(struct connectivity_progress *cp) · line 50

Quoted 10 lines
reachable.c +10 -0
+               strbuf_release(&buf);
+               return 0;
+       }
+       strbuf_trim(&buf);
+       if (!get_oid_hex(buf.buf, &oid)) {
+               object = parse_object_or_die(&oid, buf.buf);
+               add_pending_object(revs, object, "");
+       }
+       return 0;
+}

Is this leaking the strbuf? Should the function instead end with:

Quoted 2 lines
     strbuf_release(&buf);
     return 0;

Yes, well spotted

Quoted 3 lines

Also, what is the significance of the return value of this function? All code paths seem to be returning 0 unconditionally, and the caller ignores the return value.

Good point, I think in an earlier draft it returned an error at one point, I'll change the return types.

Quoted 3 lines
Quoted 1 line

+/* Mark objects recored in rebase state files as reachable. */

s/recored/recorded/

Sharp eyes as ever, thanks for looking at this I'll re-roll

Phillip

#3 lore raw
In reply to Phillip Wood via GitGitGadget
Quoted 6 lines · Phillip Wood via GitGitGadget

Rebase records the oid of HEAD before rebasing and the commit created by "--autostash" in files in the rebase state directory. This means that the autostash commit is never reachable from any ref or reflog and when rebasing a detached HEAD the original HEAD can become unreachable if the user expires HEAD's the reflog while the rebase is running. Fix this by reading the relevant files when marking reachable commits.

I do not like this kind of special casing in general, but because these are our tools' droppings, I am OK to grandfather them in, as long as we promise ourselves that we will not add more of these ad-hoc "text files" that record object names, loss of which affects correctness. They should, like "git bisect", be using proper references to protect these objects instead, of course.

I agree with you that we might want to add pseudorefs as a starting points of reachability traversal, but I suspect it would add unnecessary complexity we would rather not want to deal with.

For example, not GC'ing what is pointed at by lines in FETCH_HEAD is OK. Excluding those objects that are only reachable from an object mentioned by a pseudoref, when a new "git fetch" is negotiating with a remote what objects need to be sent here, might be disastrous, as the pseudoref that said "this object is here and you can safely consider everything reachable from it is" will be short-lived and can go away anytime, and an auto-gc kicking in at a wrong time ...

Thanks.

#4 lore raw

Hi Junio

Quoted 9 lines · Junio C Hamano
Quoted 6 lines · Phillip Wood via GitGitGadget

Rebase records the oid of HEAD before rebasing and the commit created by "--autostash" in files in the rebase state directory. This means that the autostash commit is never reachable from any ref or reflog and when rebasing a detached HEAD the original HEAD can become unreachable if the user expires HEAD's the reflog while the rebase is running. Fix this by reading the relevant files when marking reachable commits.

Quoted 6 lines

I do not like this kind of special casing in general, but because these are our tools' droppings, I am OK to grandfather them in, as long as we promise ourselves that we will not add more of these ad-hoc "text files" that record object names, loss of which affects correctness. They should, like "git bisect", be using proper references to protect these objects instead, of course.

We should definitely do that for future commands

Quoted 11 lines

I agree with you that we might want to add pseudorefs as a starting points of reachability traversal, but I suspect it would add unnecessary complexity we would rather not want to deal with.

For example, not GC'ing what is pointed at by lines in FETCH_HEAD is OK. Excluding those objects that are only reachable from an object mentioned by a pseudoref, when a new "git fetch" is negotiating with a remote what objects need to be sent here, might be disastrous, as the pseudoref that said "this object is here and you can safely consider everything reachable from it is" will be short-lived and can go away anytime, and an auto-gc kicking in at a wrong time ...

I can see that including pseudorefs when "git fetch" is negotiating could be problematic but does it use mark_reachable_objects()? Maybe I'm missing something as I've only done a quick grep but it only seems to be called from builtin/prune.c and builtin/repack.c and prior to 4421474e06 (Move traversal of reachable objects into a separate library., 2007-01-06) it seems to have been a static method in builtin-prune.c

Best Wishes

Phillip

#5 lore raw
In reply to Junio C Hamano
Quoted 5 lines · Junio C Hamano
Quoted 2 lines · Phillip Wood via GitGitGadget

Rebase records the oid of HEAD before rebasing and the commit created by "--autostash" in files in the rebase state directory. This means that

...

Quoted 6 lines

I do not like this kind of special casing in general, but because these are our tools' droppings, I am OK to grandfather them in, as long as we promise ourselves that we will not add more of these ad-hoc "text files" that record object names, loss of which affects correctness. They should, like "git bisect", be using proper references to protect these objects instead, of course.

I have long wanted to have a special ref named "AUTOSTASH" since it supports my workflow of applying workdir changes to previous commits during a rebase. For example, I often do this:

     $ git rebase -i
     Created autostash: c0ffeebea0
     ...
     <stopped to edit some commit in my history>
     $ git stash apply c0ffeebea0
     $ git commit --amend && git rebase --continue

But it requires me to find the text output after "Created autostash:" from the original rebase command which may have scrolled a lot by now. It would be easier to say:

     $ git stash apply AUTOSTASH

I see that MERGE_AUTOSTASH has been added lately. And I am inferring that there's a desire to remove (eventually) these file-based info trackers such as "rebase-apply/autostash". Is there any reason not to raise the rebase/autostash notation to a proper ref now? Should it be named REBASE_AUTOSTASH if I add this?

Even if we don't remove the file-based notation immediately "rebase-apply/autostash", I would like to add a ref that duplicates the information for my workflow. Maybe we can deprecate the file itself and remove it in some future version.

#6 lore raw

PATCH v2 1/1 prune: mark rebase autostash and orig-head as reachable

In reply to Phillip Wood via GitGitGadget

From: Phillip Wood <[email protected]>

Rebase records the oid of HEAD before rebasing and the commit created by "--autostash" in files in the rebase state directory. This means that the autostash commit is never reachable from any ref or reflog and when rebasing a detached HEAD the original HEAD can become unreachable if the user expires HEAD's the reflog while the rebase is running. Fix this by reading the relevant files when marking reachable commits.

Note that it is possible for the commit recorded in .git/rebase-merge/amend to be unreachable but pruning that object does not affect the operation of "git rebase --continue" as we're only interested in the object id, not in the object itself.

---
prune: mark rebase autostash and orig-head as reachable

Thanks for the comments on v1. I've fixed the memory leak and changed the return types as suggested by Eric.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1656%2Fphillipwood%2Fprune-protect-rebase-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1656/phillipwood/prune-protect-rebase-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1656

Range-diff vs v1:

1: ce75996f1a3 ! 1: 7256197a26f prune: mark rebase autostash and orig-head as reachable

     @@ reachable.c: static void update_progress(struct connectivity_progress *cp)
       		display_progress(cp->progress, cp->count);
       }
       
     -+static int add_one_file(const char *path, struct rev_info *revs)
     ++static void add_one_file(const char *path, struct rev_info *revs)
      +{
      +	struct strbuf buf = STRBUF_INIT;
      +	struct object_id oid;
     @@ reachable.c: static void update_progress(struct connectivity_progress *cp)
      +
      +	if (!read_oneliner(&buf, path, READ_ONELINER_SKIP_IF_EMPTY)) {
      +		strbuf_release(&buf);
     -+		return 0;
     ++		return;
      +	}
      +	strbuf_trim(&buf);
      +	if (!get_oid_hex(buf.buf, &oid)) {
      +		object = parse_object_or_die(&oid, buf.buf);
      +		add_pending_object(revs, object, "");
      +	}
     -+	return 0;
     ++	strbuf_release(&buf);
      +}
      +
     -+/* Mark objects recored in rebase state files as reachable. */
     -+static int add_rebase_files(struct rev_info *revs)
     ++/* Mark objects recorded in rebase state files as reachable. */
     ++static void add_rebase_files(struct rev_info *revs)
      +{
      +	struct strbuf buf = STRBUF_INIT;
      +	size_t len;
     @@ reachable.c: static void update_progress(struct connectivity_progress *cp)
      +	}
      +	strbuf_release(&buf);
      +	free_worktrees(worktrees);
     -+	return 0;
      +}
      +
       static int add_one_ref(const char *path, const struct object_id *oid,
 reachable.c                 | 50 +++++++++++++++++++++++++++++++++++++
 t/t3407-rebase-abort.sh     | 17 ++++++++++++-
 t/t3420-rebase-autostash.sh | 10 ++++++++
 3 files changed, 76 insertions(+), 1 deletion(-)
reachable.c +50 -0
index f29b06a5d05..3b85add243b 100644
@@ -17,6 +17,7 @@
 #include "pack-mtimes.h"
 #include "config.h"
 #include "run-command.h"
+#include "sequencer.h"
 
 struct connectivity_progress {
 	struct progress *progress;
@@ -30,6 +31,52 @@ static void update_progress(struct connectivity_progress *cp)
 		display_progress(cp->progress, cp->count);
 }
 
+static void add_one_file(const char *path, struct rev_info *revs)
+{
+	struct strbuf buf = STRBUF_INIT;
+	struct object_id oid;
+	struct object *object;
+
+	if (!read_oneliner(&buf, path, READ_ONELINER_SKIP_IF_EMPTY)) {
+		strbuf_release(&buf);
+		return;
+	}
+	strbuf_trim(&buf);
+	if (!get_oid_hex(buf.buf, &oid)) {
+		object = parse_object_or_die(&oid, buf.buf);
+		add_pending_object(revs, object, "");
+	}
+	strbuf_release(&buf);
+}
+
+/* Mark objects recorded in rebase state files as reachable. */
+static void add_rebase_files(struct rev_info *revs)
+{
+	struct strbuf buf = STRBUF_INIT;
+	size_t len;
+	const char *path[] = {
+		"rebase-apply/autostash",
+		"rebase-apply/orig-head",
+		"rebase-merge/autostash",
+		"rebase-merge/orig-head",
+	};
+	struct worktree **worktrees = get_worktrees();
+
+	for (struct worktree **wt = worktrees; *wt; wt++) {
+		strbuf_reset(&buf);
+		strbuf_addstr(&buf, get_worktree_git_dir(*wt));
+		strbuf_complete(&buf, '/');
+		len = buf.len;
+		for (size_t i = 0; i < ARRAY_SIZE(path); i++) {
+			strbuf_setlen(&buf, len);
+			strbuf_addstr(&buf, path[i]);
+			add_one_file(buf.buf, revs);
+		}
+	}
+	strbuf_release(&buf);
+	free_worktrees(worktrees);
+}
+
 static int add_one_ref(const char *path, const struct object_id *oid,
 		       int flag, void *cb_data)
 {
@@ -322,6 +369,9 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog,
 	head_ref(add_one_ref, revs);
 	other_head_refs(add_one_ref, revs);
 
+	/* rebase autostash and orig-head */
+	add_rebase_files(revs);
+
 	/* Add all reflog info */
 	if (mark_reflog)
 		add_reflogs_to_pending(revs, 0);
t/t3407-rebase-abort.sh +16 -1
index ebbaed147a6..9f49c4228b6 100755
@@ -40,9 +40,24 @@ testrebase() {
 		test_path_is_missing "$state_dir"
 	'
 
+	test_expect_success "pre rebase$type head is marked as reachable" '
+		# Clean up the state from the previous one
+		git checkout -f --detach pre-rebase &&
+		test_tick &&
+		git commit --amend --only -m "reworded" &&
+		orig_head=$(git rev-parse HEAD) &&
+		test_must_fail git rebase$type main &&
+		# Stop ORIG_HEAD marking $state_dir/orig-head as reachable
+		git update-ref -d ORIG_HEAD &&
+		git reflog expire --expire="$GIT_COMMITTER_DATE" --all &&
+		git prune --expire=now &&
+		git rebase --abort &&
+		test_cmp_rev $orig_head HEAD
+	'
+
 	test_expect_success "rebase$type --abort after --skip" '
 		# Clean up the state from the previous one
-		git reset --hard pre-rebase &&
+		git checkout -B to-rebase pre-rebase &&
 		test_must_fail git rebase$type main &&
 		test_path_is_dir "$state_dir" &&
 		test_must_fail git rebase --skip &&
t/t3420-rebase-autostash.sh +10 -0
index 693934ee8be..1a820f14815 100755
@@ -333,4 +333,14 @@ test_expect_success 'never change active branch' '
 	test_cmp_rev not-the-feature-branch unrelated-onto-branch
 '
 
+test_expect_success 'autostash commit is marked as reachable' '
+	echo changed >file0 &&
+	git rebase --autostash --exec "git prune --expire=now" \
+		feature-branch^ feature-branch &&
+	# git rebase succeeds if the stash cannot be applied so we need to check
+	# the contents of file0
+	echo changed >expect &&
+	test_cmp expect file0
+'
+
 test_done

base-commit: 2a540e432fe5dff3cfa9d3bf7ca56db2ad12ebb9

Signature · 1 line
gitgitgadget
#7 lore raw

reachable.c static void update_progress(struct connectivity_progress *cp) · line 62

Quoted 10 lines · Phillip Wood via GitGitGadget
reachable.c +10 -0
+static void add_rebase_files(struct rev_info *revs)
+{
+	struct strbuf buf = STRBUF_INIT;
+	size_t len;
+	const char *path[] = {
+		"rebase-apply/autostash",
+		"rebase-apply/orig-head",
+		"rebase-merge/autostash",
+		"rebase-merge/orig-head",
+	};

Yuck.

Having this table here makes the sequencer subsystem even less maintainable than it already is. I wonder if we can at least somehow share some of these? #leftoverbits.

Thanks.