prune: mark rebase autostash and orig-head as reachable

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. Reported-by: Orgad Shaneh <orgads@gmail.com> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Phillip Wood committed Feb 9, 2024 at 16:19 UTC bc7f5db896f59275fb0e4093dfd6891bfcece63d
3 files changed +76 -1
reachable.c
+50
@@ -17,6 +17,7 @@
17 #include "pack-mtimes.h"
18 #include "config.h"
19 #include "run-command.h"
20 +#include "sequencer.h"
21
22 struct connectivity_progress {
23 struct progress *progress;
@@ -30,6 +31,52 @@ static void update_progress(struct connectivity_progress *cp)
31 display_progress(cp->progress, cp->count);
32 }
33
34 +static void add_one_file(const char *path, struct rev_info *revs)
35 +{
36 + struct strbuf buf = STRBUF_INIT;
37 + struct object_id oid;
38 + struct object *object;
39 +
40 + if (!read_oneliner(&buf, path, READ_ONELINER_SKIP_IF_EMPTY)) {
41 + strbuf_release(&buf);
42 + return;
43 + }
44 + strbuf_trim(&buf);
45 + if (!get_oid_hex(buf.buf, &oid)) {
46 + object = parse_object_or_die(&oid, buf.buf);
47 + add_pending_object(revs, object, "");
48 + }
49 + strbuf_release(&buf);
50 +}
51 +
52 +/* Mark objects recorded in rebase state files as reachable. */
53 +static void add_rebase_files(struct rev_info *revs)
54 +{
55 + struct strbuf buf = STRBUF_INIT;
56 + size_t len;
57 + const char *path[] = {
58 + "rebase-apply/autostash",
59 + "rebase-apply/orig-head",
60 + "rebase-merge/autostash",
61 + "rebase-merge/orig-head",
62 + };
63 + struct worktree **worktrees = get_worktrees();
64 +
65 + for (struct worktree **wt = worktrees; *wt; wt++) {
66 + strbuf_reset(&buf);
67 + strbuf_addstr(&buf, get_worktree_git_dir(*wt));
68 + strbuf_complete(&buf, '/');
69 + len = buf.len;
70 + for (size_t i = 0; i < ARRAY_SIZE(path); i++) {
71 + strbuf_setlen(&buf, len);
72 + strbuf_addstr(&buf, path[i]);
73 + add_one_file(buf.buf, revs);
74 + }
75 + }
76 + strbuf_release(&buf);
77 + free_worktrees(worktrees);
78 +}
79 +
80 static int add_one_ref(const char *path, const struct object_id *oid,
81 int flag, void *cb_data)
82 {
@@ -322,6 +369,9 @@ void mark_reachable_objects(struct rev_info *revs, int mark_reflog,
369 head_ref(add_one_ref, revs);
370 other_head_refs(add_one_ref, revs);
371
372 + /* rebase autostash and orig-head */
373 + add_rebase_files(revs);
374 +
375 /* Add all reflog info */
376 if (mark_reflog)
377 add_reflogs_to_pending(revs, 0);
t/t3407-rebase-abort.sh
+16 -1
@@ -40,9 +40,24 @@ testrebase() {
40 test_path_is_missing "$state_dir"
41 '
42
43 + test_expect_success "pre rebase$type head is marked as reachable" '
44 + # Clean up the state from the previous one
45 + git checkout -f --detach pre-rebase &&
46 + test_tick &&
47 + git commit --amend --only -m "reworded" &&
48 + orig_head=$(git rev-parse HEAD) &&
49 + test_must_fail git rebase$type main &&
50 + # Stop ORIG_HEAD marking $state_dir/orig-head as reachable
51 + git update-ref -d ORIG_HEAD &&
52 + git reflog expire --expire="$GIT_COMMITTER_DATE" --all &&
53 + git prune --expire=now &&
54 + git rebase --abort &&
55 + test_cmp_rev $orig_head HEAD
56 + '
57 +
58 test_expect_success "rebase$type --abort after --skip" '
59 # Clean up the state from the previous one
45 - git reset --hard pre-rebase &&
60 + git checkout -B to-rebase pre-rebase &&
61 test_must_fail git rebase$type main &&
62 test_path_is_dir "$state_dir" &&
63 test_must_fail git rebase --skip &&
t/t3420-rebase-autostash.sh
+10
@@ -333,4 +333,14 @@ test_expect_success 'never change active branch' '
333 test_cmp_rev not-the-feature-branch unrelated-onto-branch
334 '
335
336 +test_expect_success 'autostash commit is marked as reachable' '
337 + echo changed >file0 &&
338 + git rebase --autostash --exec "git prune --expire=now" \
339 + feature-branch^ feature-branch &&
340 + # git rebase succeeds if the stash cannot be applied so we need to check
341 + # the contents of file0
342 + echo changed >expect &&
343 + test_cmp expect file0
344 +'
345 +
346 test_done