From ae42fa4c03ef0ffe36468f8918b3d404a277da50 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:36 +0000 Subject: [PATCH 01/14] rebase: factor out checkout for up to date branch This code is heavily indented and it will be convenient later in the series to have it in its own function. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/rebase.c | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index 34b4744e5f..f5c37b7d4a 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -812,6 +812,23 @@ static int rebase_config(const char *var, const char *value, void *data) return git_default_config(var, value, data); } +static int checkout_up_to_date(struct rebase_options *options) +{ + struct strbuf buf = STRBUF_INIT; + int ret = 0; + + strbuf_addf(&buf, "%s: checkout %s", + getenv(GIT_REFLOG_ACTION_ENVIRONMENT), + options->switch_to); + if (reset_head(the_repository, &options->orig_head, "checkout", + options->head_name, RESET_HEAD_RUN_POST_CHECKOUT_HOOK, + NULL, buf.buf, DEFAULT_REFLOG_ACTION) < 0) + ret = error(_("could not switch to %s"), options->switch_to); + strbuf_release(&buf); + + return ret; +} + /* * Determines whether the commits in from..to are linear, i.e. contain * no merge commits. This function *expects* `from` to be an ancestor of @@ -1673,21 +1690,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) if (!(options.flags & REBASE_FORCE)) { /* Lazily switch to the target branch if needed... */ if (options.switch_to) { - strbuf_reset(&buf); - strbuf_addf(&buf, "%s: checkout %s", - getenv(GIT_REFLOG_ACTION_ENVIRONMENT), - options.switch_to); - if (reset_head(the_repository, - &options.orig_head, "checkout", - options.head_name, - RESET_HEAD_RUN_POST_CHECKOUT_HOOK, - NULL, buf.buf, - DEFAULT_REFLOG_ACTION) < 0) { - ret = error(_("could not switch to " - "%s"), - options.switch_to); + ret = checkout_up_to_date(&options); + if (ret) goto cleanup; - } } if (!(options.flags & REBASE_NO_QUIET)) From bd55eee04b698af6c10c77b24f88601814771ac8 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:37 +0000 Subject: [PATCH 02/14] t5403: refactor rebase post-checkout hook tests These tests only test the default backend and do not check that the arguments passed to the hook are correct. Fix this by running the tests with both backends and adding checks for the hook arguments. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- t/t5403-post-checkout-hook.sh | 42 ++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/t/t5403-post-checkout-hook.sh b/t/t5403-post-checkout-hook.sh index 1ec9e23be7..272b02687b 100755 --- a/t/t5403-post-checkout-hook.sh +++ b/t/t5403-post-checkout-hook.sh @@ -49,23 +49,33 @@ test_expect_success 'post-checkout receives the right args when not switching br test $old = $new && test $flag = 0 ' -test_expect_success 'post-checkout is triggered on rebase' ' - test_when_finished "rm -f .git/post-checkout.args" && - git checkout -b rebase-test main && - rm -f .git/post-checkout.args && - git rebase rebase-on-me && - read old new flag <.git/post-checkout.args && - test $old != $new && test $flag = 1 -' +test_rebase () { + args="$*" && + test_expect_success "post-checkout is triggered on rebase $args" ' + test_when_finished "rm -f .git/post-checkout.args" && + git checkout -B rebase-test main && + rm -f .git/post-checkout.args && + git rebase $args rebase-on-me && + read old new flag <.git/post-checkout.args && + test_cmp_rev main $old && + test_cmp_rev rebase-on-me $new && + test $flag = 1 + ' -test_expect_success 'post-checkout is triggered on rebase with fast-forward' ' - test_when_finished "rm -f .git/post-checkout.args" && - git checkout -b ff-rebase-test rebase-on-me^ && - rm -f .git/post-checkout.args && - git rebase rebase-on-me && - read old new flag <.git/post-checkout.args && - test $old != $new && test $flag = 1 -' + test_expect_success "post-checkout is triggered on rebase $args with fast-forward" ' + test_when_finished "rm -f .git/post-checkout.args" && + git checkout -B ff-rebase-test rebase-on-me^ && + rm -f .git/post-checkout.args && + git rebase $args rebase-on-me && + read old new flag <.git/post-checkout.args && + test_cmp_rev rebase-on-me^ $old && + test_cmp_rev rebase-on-me $new && + test $flag = 1 + ' +} + +test_rebase --apply && +test_rebase --merge test_expect_success 'post-checkout hook is triggered by clone' ' mkdir -p templates/hooks && From 69f4c23009ee30faeb61a831f4265791c945783e Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:38 +0000 Subject: [PATCH 03/14] rebase: pass correct arguments to post-checkout hook If a rebase started with "rebase [--apply|--merge] " detects that is an ancestor of then it fast-forwards and checks out . Unfortunately in that case it passed the null oid as the first argument to the post-checkout hook rather than the oid of HEAD. A side effect of this change is that the call to update_ref() which updates HEAD now always receives the old value of HEAD. This provides protection against another process updating HEAD during the checkout. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- reset.c | 18 +++++++++--------- t/t5403-post-checkout-hook.sh | 13 +++++++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/reset.c b/reset.c index f214df3d96..315fef91d3 100644 --- a/reset.c +++ b/reset.c @@ -18,7 +18,7 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action, unsigned run_hook = flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK; unsigned refs_only = flags & RESET_HEAD_REFS_ONLY; unsigned update_orig_head = flags & RESET_ORIG_HEAD; - struct object_id head_oid; + struct object_id *head = NULL, head_oid; struct tree_desc desc[2] = { { NULL }, { NULL } }; struct lock_file lock = LOCK_INIT; struct unpack_trees_options unpack_tree_opts = { 0 }; @@ -26,8 +26,7 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action, const char *reflog_action; struct strbuf msg = STRBUF_INIT; size_t prefix_len; - struct object_id *orig = NULL, oid_orig, - *old_orig = NULL, oid_old_orig; + struct object_id *old_orig = NULL, oid_old_orig; int ret = 0, nr = 0; if (switch_to_branch && !starts_with(switch_to_branch, "refs/")) @@ -38,7 +37,9 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action, goto leave_reset_head; } - if ((!oid || !reset_hard) && get_oid("HEAD", &head_oid)) { + if (!get_oid("HEAD", &head_oid)) { + head = &head_oid; + } else if (!oid || !reset_hard) { ret = error(_("could not determine HEAD revision")); goto leave_reset_head; } @@ -98,13 +99,12 @@ reset_head_refs: if (update_orig_head) { if (!get_oid("ORIG_HEAD", &oid_old_orig)) old_orig = &oid_old_orig; - if (!get_oid("HEAD", &oid_orig)) { - orig = &oid_orig; + if (head) { if (!reflog_orig_head) { strbuf_addstr(&msg, "updating ORIG_HEAD"); reflog_orig_head = msg.buf; } - update_ref(reflog_orig_head, "ORIG_HEAD", orig, + update_ref(reflog_orig_head, "ORIG_HEAD", head, old_orig, 0, UPDATE_REFS_MSG_ON_ERR); } else if (old_orig) delete_ref(NULL, "ORIG_HEAD", old_orig, 0); @@ -116,7 +116,7 @@ reset_head_refs: reflog_head = msg.buf; } if (!switch_to_branch) - ret = update_ref(reflog_head, "HEAD", oid, orig, + ret = update_ref(reflog_head, "HEAD", oid, head, detach_head ? REF_NO_DEREF : 0, UPDATE_REFS_MSG_ON_ERR); else { @@ -128,7 +128,7 @@ reset_head_refs: } if (run_hook) run_hook_le(NULL, "post-checkout", - oid_to_hex(orig ? orig : null_oid()), + oid_to_hex(head ? head : null_oid()), oid_to_hex(oid), "1", NULL); leave_reset_head: diff --git a/t/t5403-post-checkout-hook.sh b/t/t5403-post-checkout-hook.sh index 272b02687b..17ab518f26 100755 --- a/t/t5403-post-checkout-hook.sh +++ b/t/t5403-post-checkout-hook.sh @@ -72,6 +72,19 @@ test_rebase () { test_cmp_rev rebase-on-me $new && test $flag = 1 ' + + test_expect_success "rebase $args fast-forward branch checkout runs post-checkout hook" ' + test_when_finished "test_might_fail git rebase --abort" && + test_when_finished "rm -f .git/post-checkout.args" && + git update-ref refs/heads/rebase-fast-forward three && + git checkout two && + rm -f .git/post-checkout.args && + git rebase $args HEAD rebase-fast-forward && + read old new flag <.git/post-checkout.args && + test_cmp_rev two $old && + test_cmp_rev three $new && + test $flag = 1 + ' } test_rebase --apply && From ab2fba0868860e610619f885031217d9cc63097a Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:39 +0000 Subject: [PATCH 04/14] rebase: do not remove untracked files on checkout If "git rebase [--apply|--merge] " detects that is an ancestor of then it will fast-forward and checkout . Normally a checkout or picking a commit during a rebase will refuse to overwrite untracked files, however rebase does overwrite untracked files when checking out . The fix is to only set reset in `unpack_tree_opts` if flags contains `RESET_HEAD_HARD`. t5403 may seem like an odd home for the new test but it will be extended in the next commit to check that the post-checkout hook is not run when the checkout fails. The test for `!detach_head` dates back to the original implementation of reset_head() in ac7f467fef ("builtin/rebase: support running "git rebase "", 2018-08-07) and was correct until e65123a71d ("builtin rebase: support `git rebase `", 2018-09-04) started using reset_head() to checkout when fast-forwarding. Note that 480d3d6bf9 ("Change unpack_trees' 'reset' flag into an enum", 2021-09-27) also fixes this bug as it changes reset_head() to never remove untracked files. I think this fix is still worthwhile as it makes it clear that the same settings are used for detached and non-detached checkouts. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- reset.c | 2 +- t/t5403-post-checkout-hook.sh | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/reset.c b/reset.c index 315fef91d3..3e7b9e2e13 100644 --- a/reset.c +++ b/reset.c @@ -59,7 +59,7 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action, unpack_tree_opts.merge = 1; unpack_tree_opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL); - if (!detach_head) + if (reset_hard) unpack_tree_opts.reset = UNPACK_RESET_PROTECT_UNTRACKED; if (repo_read_index_unmerged(r) < 0) { diff --git a/t/t5403-post-checkout-hook.sh b/t/t5403-post-checkout-hook.sh index 17ab518f26..fd2817b406 100755 --- a/t/t5403-post-checkout-hook.sh +++ b/t/t5403-post-checkout-hook.sh @@ -85,6 +85,16 @@ test_rebase () { test_cmp_rev three $new && test $flag = 1 ' + + test_expect_success "rebase $args checkout does not remove untracked files" ' + test_when_finished "test_might_fail git rebase --abort" && + git update-ref refs/heads/rebase-fast-forward three && + git checkout two && + echo untracked >three.t && + test_when_finished "rm three.t" && + test_must_fail git rebase $args HEAD rebase-fast-forward 2>err && + grep "untracked working tree files would be overwritten by checkout" err +' } test_rebase --apply && From 4840002a5f44a4c256c55f70c59d3b0506d14e21 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:40 +0000 Subject: [PATCH 05/14] rebase --apply: don't run post-checkout hook if there is an error The hook should only be run if the worktree and refs were successfully updated. This primarily affects "rebase --apply" but also "rebase --merge" when it fast-forwards. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- reset.c | 2 +- t/t5403-post-checkout-hook.sh | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/reset.c b/reset.c index 3e7b9e2e13..3537de91f6 100644 --- a/reset.c +++ b/reset.c @@ -126,7 +126,7 @@ reset_head_refs: ret = create_symref("HEAD", switch_to_branch, reflog_head); } - if (run_hook) + if (!ret && run_hook) run_hook_le(NULL, "post-checkout", oid_to_hex(head ? head : null_oid()), oid_to_hex(oid), "1", NULL); diff --git a/t/t5403-post-checkout-hook.sh b/t/t5403-post-checkout-hook.sh index fd2817b406..d118181690 100755 --- a/t/t5403-post-checkout-hook.sh +++ b/t/t5403-post-checkout-hook.sh @@ -88,12 +88,16 @@ test_rebase () { test_expect_success "rebase $args checkout does not remove untracked files" ' test_when_finished "test_might_fail git rebase --abort" && + test_when_finished "rm -f .git/post-checkout.args" && git update-ref refs/heads/rebase-fast-forward three && git checkout two && + rm -f .git/post-checkout.args && echo untracked >three.t && test_when_finished "rm three.t" && test_must_fail git rebase $args HEAD rebase-fast-forward 2>err && - grep "untracked working tree files would be overwritten by checkout" err + grep "untracked working tree files would be overwritten by checkout" err && + test_path_is_missing .git/post-checkout.args + ' } From 1946d45844c65ede4e3a514a5decf16612ad79f0 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:41 +0000 Subject: [PATCH 06/14] reset_head(): remove action parameter The only use of the action parameter is to setup the error messages for unpack_trees(). All but two cases pass either "checkout" or "reset". The case that passes "reset --hard" would be better passing "reset" so that the error messages match the builtin reset command like all the other callers that are doing a reset. The case that passes "Fast-forwarded" is only updating HEAD and so the parameter is unused in that case as it does not call unpack_trees(). The value to pass to setup_unpack_trees_porcelain() can be determined by checking whether flags contains RESET_HEAD_HARD without the caller having to specify it. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/rebase.c | 14 +++++++------- reset.c | 5 +++-- reset.h | 2 +- sequencer.c | 3 +-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index f5c37b7d4a..2e5a535b54 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -583,7 +583,7 @@ static int move_to_original_branch(struct rebase_options *opts) opts->head_name, oid_to_hex(&opts->onto->object.oid)); strbuf_addf(&head_reflog, "rebase finished: returning to %s", opts->head_name); - ret = reset_head(the_repository, NULL, "", opts->head_name, + ret = reset_head(the_repository, NULL, opts->head_name, RESET_HEAD_REFS_ONLY, orig_head_reflog.buf, head_reflog.buf, DEFAULT_REFLOG_ACTION); @@ -674,7 +674,7 @@ static int run_am(struct rebase_options *opts) free(rebased_patches); strvec_clear(&am.args); - reset_head(the_repository, &opts->orig_head, "checkout", + reset_head(the_repository, &opts->orig_head, opts->head_name, 0, "HEAD", NULL, DEFAULT_REFLOG_ACTION); error(_("\ngit encountered an error while preparing the " @@ -820,7 +820,7 @@ static int checkout_up_to_date(struct rebase_options *options) strbuf_addf(&buf, "%s: checkout %s", getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options->switch_to); - if (reset_head(the_repository, &options->orig_head, "checkout", + if (reset_head(the_repository, &options->orig_head, options->head_name, RESET_HEAD_RUN_POST_CHECKOUT_HOOK, NULL, buf.buf, DEFAULT_REFLOG_ACTION) < 0) ret = error(_("could not switch to %s"), options->switch_to); @@ -1272,7 +1272,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) rerere_clear(the_repository, &merge_rr); string_list_clear(&merge_rr, 1); - if (reset_head(the_repository, NULL, "reset", NULL, RESET_HEAD_HARD, + if (reset_head(the_repository, NULL, NULL, RESET_HEAD_HARD, NULL, NULL, DEFAULT_REFLOG_ACTION) < 0) die(_("could not discard worktree changes")); remove_branch_state(the_repository, 0); @@ -1290,7 +1290,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) if (read_basic_state(&options)) exit(1); - if (reset_head(the_repository, &options.orig_head, "reset", + if (reset_head(the_repository, &options.orig_head, options.head_name, RESET_HEAD_HARD, NULL, NULL, DEFAULT_REFLOG_ACTION) < 0) die(_("could not move back to %s"), @@ -1759,7 +1759,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) strbuf_addf(&msg, "%s: checkout %s", getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options.onto_name); - if (reset_head(the_repository, &options.onto->object.oid, "checkout", NULL, + if (reset_head(the_repository, &options.onto->object.oid, NULL, RESET_HEAD_DETACH | RESET_ORIG_HEAD | RESET_HEAD_RUN_POST_CHECKOUT_HOOK, NULL, msg.buf, DEFAULT_REFLOG_ACTION)) @@ -1777,7 +1777,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) strbuf_addf(&msg, "rebase finished: %s onto %s", options.head_name ? options.head_name : "detached HEAD", oid_to_hex(&options.onto->object.oid)); - reset_head(the_repository, NULL, "Fast-forwarded", options.head_name, + reset_head(the_repository, NULL, options.head_name, RESET_HEAD_REFS_ONLY, "HEAD", msg.buf, DEFAULT_REFLOG_ACTION); strbuf_release(&msg); diff --git a/reset.c b/reset.c index 3537de91f6..7841b2b2a0 100644 --- a/reset.c +++ b/reset.c @@ -8,7 +8,7 @@ #include "tree.h" #include "unpack-trees.h" -int reset_head(struct repository *r, struct object_id *oid, const char *action, +int reset_head(struct repository *r, struct object_id *oid, const char *switch_to_branch, unsigned flags, const char *reflog_orig_head, const char *reflog_head, const char *default_reflog_action) @@ -23,7 +23,7 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action, struct lock_file lock = LOCK_INIT; struct unpack_trees_options unpack_tree_opts = { 0 }; struct tree *tree; - const char *reflog_action; + const char *action, *reflog_action; struct strbuf msg = STRBUF_INIT; size_t prefix_len; struct object_id *old_orig = NULL, oid_old_orig; @@ -50,6 +50,7 @@ int reset_head(struct repository *r, struct object_id *oid, const char *action, if (refs_only) goto reset_head_refs; + action = reset_hard ? "reset" : "checkout"; setup_unpack_trees_porcelain(&unpack_tree_opts, action); unpack_tree_opts.head_idx = 1; unpack_tree_opts.src_index = r->index; diff --git a/reset.h b/reset.h index 12f83c78e2..2daec80425 100644 --- a/reset.h +++ b/reset.h @@ -12,7 +12,7 @@ #define RESET_HEAD_REFS_ONLY (1<<3) #define RESET_ORIG_HEAD (1<<4) -int reset_head(struct repository *r, struct object_id *oid, const char *action, +int reset_head(struct repository *r, struct object_id *oid, const char *switch_to_branch, unsigned flags, const char *reflog_orig_head, const char *reflog_head, const char *default_reflog_action); diff --git a/sequencer.c b/sequencer.c index b4135a78c9..e18329a399 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4137,8 +4137,7 @@ void create_autostash(struct repository *r, const char *path, path); write_file(path, "%s", oid_to_hex(&oid)); printf(_("Created autostash: %s\n"), buf.buf); - if (reset_head(r, NULL, "reset --hard", - NULL, RESET_HEAD_HARD, NULL, NULL, + if (reset_head(r, NULL, NULL, RESET_HEAD_HARD, NULL, NULL, default_reflog_action) < 0) die(_("could not reset --hard")); From d6a9f5ea8e97dd9435e5fa02cc129c1b241934f2 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:42 +0000 Subject: [PATCH 07/14] reset_head(): factor out ref updates In the next commit we will stop trying to update HEAD when we are removing uncommitted changes from the working tree. Move the code that updates the refs to its own function in preparation for that. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- reset.c | 110 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/reset.c b/reset.c index 7841b2b2a0..56d6e2a06d 100644 --- a/reset.c +++ b/reset.c @@ -8,25 +8,75 @@ #include "tree.h" #include "unpack-trees.h" +static int update_refs(const struct object_id *oid, const char *switch_to_branch, + const struct object_id *head, const char *reflog_head, + const char *reflog_orig_head, + const char *default_reflog_action, unsigned flags) +{ + unsigned detach_head = flags & RESET_HEAD_DETACH; + unsigned run_hook = flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK; + unsigned update_orig_head = flags & RESET_ORIG_HEAD; + struct object_id *old_orig = NULL, oid_old_orig; + struct strbuf msg = STRBUF_INIT; + const char *reflog_action; + size_t prefix_len; + int ret; + + reflog_action = getenv(GIT_REFLOG_ACTION_ENVIRONMENT); + strbuf_addf(&msg, "%s: ", reflog_action ? reflog_action : default_reflog_action); + prefix_len = msg.len; + + if (update_orig_head) { + if (!get_oid("ORIG_HEAD", &oid_old_orig)) + old_orig = &oid_old_orig; + if (head) { + if (!reflog_orig_head) { + strbuf_addstr(&msg, "updating ORIG_HEAD"); + reflog_orig_head = msg.buf; + } + update_ref(reflog_orig_head, "ORIG_HEAD", head, + old_orig, 0, UPDATE_REFS_MSG_ON_ERR); + } else if (old_orig) + delete_ref(NULL, "ORIG_HEAD", old_orig, 0); + } + + if (!reflog_head) { + strbuf_setlen(&msg, prefix_len); + strbuf_addstr(&msg, "updating HEAD"); + reflog_head = msg.buf; + } + if (!switch_to_branch) + ret = update_ref(reflog_head, "HEAD", oid, head, + detach_head ? REF_NO_DEREF : 0, + UPDATE_REFS_MSG_ON_ERR); + else { + ret = update_ref(reflog_head, switch_to_branch, oid, + NULL, 0, UPDATE_REFS_MSG_ON_ERR); + if (!ret) + ret = create_symref("HEAD", switch_to_branch, + reflog_head); + } + if (!ret && run_hook) + run_hook_le(NULL, "post-checkout", + oid_to_hex(head ? head : null_oid()), + oid_to_hex(oid), "1", NULL); + strbuf_release(&msg); + return ret; +} + int reset_head(struct repository *r, struct object_id *oid, const char *switch_to_branch, unsigned flags, const char *reflog_orig_head, const char *reflog_head, const char *default_reflog_action) { - unsigned detach_head = flags & RESET_HEAD_DETACH; unsigned reset_hard = flags & RESET_HEAD_HARD; - unsigned run_hook = flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK; unsigned refs_only = flags & RESET_HEAD_REFS_ONLY; - unsigned update_orig_head = flags & RESET_ORIG_HEAD; struct object_id *head = NULL, head_oid; struct tree_desc desc[2] = { { NULL }, { NULL } }; struct lock_file lock = LOCK_INIT; struct unpack_trees_options unpack_tree_opts = { 0 }; struct tree *tree; - const char *action, *reflog_action; - struct strbuf msg = STRBUF_INIT; - size_t prefix_len; - struct object_id *old_orig = NULL, oid_old_orig; + const char *action; int ret = 0, nr = 0; if (switch_to_branch && !starts_with(switch_to_branch, "refs/")) @@ -48,7 +98,9 @@ int reset_head(struct repository *r, struct object_id *oid, oid = &head_oid; if (refs_only) - goto reset_head_refs; + return update_refs(oid, switch_to_branch, head, reflog_head, + reflog_orig_head, default_reflog_action, + flags); action = reset_hard ? "reset" : "checkout"; setup_unpack_trees_porcelain(&unpack_tree_opts, action); @@ -92,48 +144,10 @@ int reset_head(struct repository *r, struct object_id *oid, goto leave_reset_head; } -reset_head_refs: - reflog_action = getenv(GIT_REFLOG_ACTION_ENVIRONMENT); - strbuf_addf(&msg, "%s: ", reflog_action ? reflog_action : default_reflog_action); - prefix_len = msg.len; - - if (update_orig_head) { - if (!get_oid("ORIG_HEAD", &oid_old_orig)) - old_orig = &oid_old_orig; - if (head) { - if (!reflog_orig_head) { - strbuf_addstr(&msg, "updating ORIG_HEAD"); - reflog_orig_head = msg.buf; - } - update_ref(reflog_orig_head, "ORIG_HEAD", head, - old_orig, 0, UPDATE_REFS_MSG_ON_ERR); - } else if (old_orig) - delete_ref(NULL, "ORIG_HEAD", old_orig, 0); - } - - if (!reflog_head) { - strbuf_setlen(&msg, prefix_len); - strbuf_addstr(&msg, "updating HEAD"); - reflog_head = msg.buf; - } - if (!switch_to_branch) - ret = update_ref(reflog_head, "HEAD", oid, head, - detach_head ? REF_NO_DEREF : 0, - UPDATE_REFS_MSG_ON_ERR); - else { - ret = update_ref(reflog_head, switch_to_branch, oid, - NULL, 0, UPDATE_REFS_MSG_ON_ERR); - if (!ret) - ret = create_symref("HEAD", switch_to_branch, - reflog_head); - } - if (!ret && run_hook) - run_hook_le(NULL, "post-checkout", - oid_to_hex(head ? head : null_oid()), - oid_to_hex(oid), "1", NULL); + ret = update_refs(oid, switch_to_branch, head, reflog_head, + reflog_orig_head, default_reflog_action, flags); leave_reset_head: - strbuf_release(&msg); rollback_lock_file(&lock); clear_unpack_trees_porcelain(&unpack_tree_opts); while (nr) From 1526d0fcfd20efca24bc96a4bc14c8d5459ec470 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:43 +0000 Subject: [PATCH 08/14] reset_head(): make default_reflog_action optional This parameter is only needed when a ref is going to be updated and the caller does not pass an explicit reflog message. Callers that are only discarding uncommitted changes in the working tree such as such as "rebase --skip" or create_autostash() do not update any refs so should not have to worry about passing this parameter. This change is not intended to have any user visible changes. The pointer comparison between `oid` and `&head_oid` checks that the caller did not pass an oid to be checked out. As no callers pass RESET_HEAD_RUN_POST_CHECKOUT_HOOK without passing an oid there are no changes to when the post-checkout hook is run. As update_ref() only updates the ref if the oid passed to it differs from the current ref there are no changes to when HEAD is updated. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/rebase.c | 10 ++++------ reset.c | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index 2e5a535b54..82be965915 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -585,8 +585,7 @@ static int move_to_original_branch(struct rebase_options *opts) opts->head_name); ret = reset_head(the_repository, NULL, opts->head_name, RESET_HEAD_REFS_ONLY, - orig_head_reflog.buf, head_reflog.buf, - DEFAULT_REFLOG_ACTION); + orig_head_reflog.buf, head_reflog.buf, NULL); strbuf_release(&orig_head_reflog); strbuf_release(&head_reflog); @@ -822,7 +821,7 @@ static int checkout_up_to_date(struct rebase_options *options) options->switch_to); if (reset_head(the_repository, &options->orig_head, options->head_name, RESET_HEAD_RUN_POST_CHECKOUT_HOOK, - NULL, buf.buf, DEFAULT_REFLOG_ACTION) < 0) + NULL, buf.buf, NULL) < 0) ret = error(_("could not switch to %s"), options->switch_to); strbuf_release(&buf); @@ -1273,7 +1272,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) string_list_clear(&merge_rr, 1); if (reset_head(the_repository, NULL, NULL, RESET_HEAD_HARD, - NULL, NULL, DEFAULT_REFLOG_ACTION) < 0) + NULL, NULL, NULL) < 0) die(_("could not discard worktree changes")); remove_branch_state(the_repository, 0); if (read_basic_state(&options)) @@ -1778,8 +1777,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) options.head_name ? options.head_name : "detached HEAD", oid_to_hex(&options.onto->object.oid)); reset_head(the_repository, NULL, options.head_name, - RESET_HEAD_REFS_ONLY, "HEAD", msg.buf, - DEFAULT_REFLOG_ACTION); + RESET_HEAD_REFS_ONLY, "HEAD", msg.buf, NULL); strbuf_release(&msg); ret = finish_rebase(&options); goto cleanup; diff --git a/reset.c b/reset.c index 56d6e2a06d..4a92e4bc30 100644 --- a/reset.c +++ b/reset.c @@ -22,8 +22,13 @@ static int update_refs(const struct object_id *oid, const char *switch_to_branch size_t prefix_len; int ret; - reflog_action = getenv(GIT_REFLOG_ACTION_ENVIRONMENT); - strbuf_addf(&msg, "%s: ", reflog_action ? reflog_action : default_reflog_action); + if ((update_orig_head && !reflog_orig_head) || !reflog_head) { + if (!default_reflog_action) + BUG("default_reflog_action must be given when reflog messages are omitted"); + reflog_action = getenv(GIT_REFLOG_ACTION_ENVIRONMENT); + strbuf_addf(&msg, "%s: ", reflog_action ? reflog_action : + default_reflog_action); + } prefix_len = msg.len; if (update_orig_head) { @@ -71,6 +76,7 @@ int reset_head(struct repository *r, struct object_id *oid, { unsigned reset_hard = flags & RESET_HEAD_HARD; unsigned refs_only = flags & RESET_HEAD_REFS_ONLY; + unsigned update_orig_head = flags & RESET_ORIG_HEAD; struct object_id *head = NULL, head_oid; struct tree_desc desc[2] = { { NULL }, { NULL } }; struct lock_file lock = LOCK_INIT; @@ -144,8 +150,10 @@ int reset_head(struct repository *r, struct object_id *oid, goto leave_reset_head; } - ret = update_refs(oid, switch_to_branch, head, reflog_head, - reflog_orig_head, default_reflog_action, flags); + if (oid != &head_oid || update_orig_head || switch_to_branch) + ret = update_refs(oid, switch_to_branch, head, reflog_head, + reflog_orig_head, default_reflog_action, + flags); leave_reset_head: rollback_lock_file(&lock); From b7de153bd9332a992baa6f937372f0b1833f61e5 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:44 +0000 Subject: [PATCH 09/14] create_autostash(): remove unneeded parameter The default_reflog parameter of create_autostash() is passed to reset_head(). However as creating a stash does not involve updating any refs the parameter is not used by reset_head(). Removing the parameter from create_autostash() simplifies the callers. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/merge.c | 6 ++---- builtin/rebase.c | 8 ++++---- sequencer.c | 5 ++--- sequencer.h | 3 +-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/builtin/merge.c b/builtin/merge.c index ea3112e0c0..cb0e4e2225 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -1565,8 +1565,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix) if (autostash) create_autostash(the_repository, - git_path_merge_autostash(the_repository), - "merge"); + git_path_merge_autostash(the_repository)); if (checkout_fast_forward(the_repository, &head_commit->object.oid, &commit->object.oid, @@ -1637,8 +1636,7 @@ int cmd_merge(int argc, const char **argv, const char *prefix) if (autostash) create_autostash(the_repository, - git_path_merge_autostash(the_repository), - "merge"); + git_path_merge_autostash(the_repository)); /* We are going to make a new commit. */ git_committer_info(IDENT_STRICT); diff --git a/builtin/rebase.c b/builtin/rebase.c index 82be965915..3d78b5c8be 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1657,10 +1657,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) if (repo_read_index(the_repository) < 0) die(_("could not read index")); - if (options.autostash) { - create_autostash(the_repository, state_dir_path("autostash", &options), - DEFAULT_REFLOG_ACTION); - } + if (options.autostash) + create_autostash(the_repository, + state_dir_path("autostash", &options)); + if (require_clean_work_tree(the_repository, "rebase", _("Please commit or stash them."), 1, 1)) { diff --git a/sequencer.c b/sequencer.c index e18329a399..119564f435 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4100,8 +4100,7 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset) return -1; } -void create_autostash(struct repository *r, const char *path, - const char *default_reflog_action) +void create_autostash(struct repository *r, const char *path) { struct strbuf buf = STRBUF_INIT; struct lock_file lock_file = LOCK_INIT; @@ -4138,7 +4137,7 @@ void create_autostash(struct repository *r, const char *path, write_file(path, "%s", oid_to_hex(&oid)); printf(_("Created autostash: %s\n"), buf.buf); if (reset_head(r, NULL, NULL, RESET_HEAD_HARD, NULL, NULL, - default_reflog_action) < 0) + NULL) < 0) die(_("could not reset --hard")); if (discard_index(r->index) < 0 || diff --git a/sequencer.h b/sequencer.h index 05a7d2ba6b..da64473636 100644 --- a/sequencer.h +++ b/sequencer.h @@ -197,8 +197,7 @@ void commit_post_rewrite(struct repository *r, const struct commit *current_head, const struct object_id *new_head); -void create_autostash(struct repository *r, const char *path, - const char *default_reflog_action); +void create_autostash(struct repository *r, const char *path); int save_autostash(const char *path); int apply_autostash(const char *path); int apply_autostash_oid(const char *stash_oid); From ee464c4e37c7f34c4e5ba2fce35df4149083e5ea Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:45 +0000 Subject: [PATCH 10/14] rebase: cleanup reset_head() calls If ORIG_HEAD is not set by passing RESET_ORIG_HEAD then there is no need to pass anything for reflog_orig_head. In addition to the callers fixed in this commit move_to_original_branch() also passes reflog_orig_head without setting ORIG_HEAD. That caller is mistakenly passing the message it wants to put in the branch reflog which is not currently possible so we delay fixing that caller until we can pass the message as the branch reflog. A later commit will make it a BUG() to pass reflog_orig_head without RESET_ORIG_HEAD, that changes cannot be done here as it needs to wait for move_to_original_branch() to be fixed first. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/rebase.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index 3d78b5c8be..fdd822c470 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -675,7 +675,7 @@ static int run_am(struct rebase_options *opts) reset_head(the_repository, &opts->orig_head, opts->head_name, 0, - "HEAD", NULL, DEFAULT_REFLOG_ACTION); + NULL, NULL, DEFAULT_REFLOG_ACTION); error(_("\ngit encountered an error while preparing the " "patches to replay\n" "these revisions:\n" @@ -1777,7 +1777,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) options.head_name ? options.head_name : "detached HEAD", oid_to_hex(&options.onto->object.oid)); reset_head(the_repository, NULL, options.head_name, - RESET_HEAD_REFS_ONLY, "HEAD", msg.buf, NULL); + RESET_HEAD_REFS_ONLY, NULL, msg.buf, NULL); strbuf_release(&msg); ret = finish_rebase(&options); goto cleanup; From 6ae8086161d81a707ff36dfdc07f57e4f473e0fd Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:46 +0000 Subject: [PATCH 11/14] reset_head(): take struct rebase_head_opts This function takes a confusingly large number of parameters which makes it difficult to remember which order to pass them in. The following commits will add a couple more parameters which makes the problem worse. To address this change the function to take a struct of options. Using a struct means that it is no longer necessary to remember which order to pass the parameters in and anyone reading the code can easily see which value is passed to each parameter. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/rebase.c | 57 ++++++++++++++++++++++++++++++------------------ reset.c | 38 +++++++++++++++----------------- reset.h | 40 +++++++++++++++++++++++++++++---- sequencer.c | 5 ++--- 4 files changed, 92 insertions(+), 48 deletions(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index fdd822c470..ecc368dd4f 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -571,6 +571,7 @@ static int finish_rebase(struct rebase_options *opts) static int move_to_original_branch(struct rebase_options *opts) { struct strbuf orig_head_reflog = STRBUF_INIT, head_reflog = STRBUF_INIT; + struct reset_head_opts ropts = { 0 }; int ret; if (!opts->head_name) @@ -583,9 +584,11 @@ static int move_to_original_branch(struct rebase_options *opts) opts->head_name, oid_to_hex(&opts->onto->object.oid)); strbuf_addf(&head_reflog, "rebase finished: returning to %s", opts->head_name); - ret = reset_head(the_repository, NULL, opts->head_name, - RESET_HEAD_REFS_ONLY, - orig_head_reflog.buf, head_reflog.buf, NULL); + ropts.branch = opts->head_name; + ropts.flags = RESET_HEAD_REFS_ONLY; + ropts.orig_head_msg = orig_head_reflog.buf; + ropts.head_msg = head_reflog.buf; + ret = reset_head(the_repository, &ropts); strbuf_release(&orig_head_reflog); strbuf_release(&head_reflog); @@ -669,13 +672,15 @@ static int run_am(struct rebase_options *opts) status = run_command(&format_patch); if (status) { + struct reset_head_opts ropts = { 0 }; unlink(rebased_patches); free(rebased_patches); strvec_clear(&am.args); - reset_head(the_repository, &opts->orig_head, - opts->head_name, 0, - NULL, NULL, DEFAULT_REFLOG_ACTION); + ropts.oid = &opts->orig_head; + ropts.branch = opts->head_name; + ropts.default_reflog_action = DEFAULT_REFLOG_ACTION; + reset_head(the_repository, &ropts); error(_("\ngit encountered an error while preparing the " "patches to replay\n" "these revisions:\n" @@ -814,14 +819,17 @@ static int rebase_config(const char *var, const char *value, void *data) static int checkout_up_to_date(struct rebase_options *options) { struct strbuf buf = STRBUF_INIT; + struct reset_head_opts ropts = { 0 }; int ret = 0; strbuf_addf(&buf, "%s: checkout %s", getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options->switch_to); - if (reset_head(the_repository, &options->orig_head, - options->head_name, RESET_HEAD_RUN_POST_CHECKOUT_HOOK, - NULL, buf.buf, NULL) < 0) + ropts.oid = &options->orig_head; + ropts.branch = options->head_name; + ropts.flags = RESET_HEAD_RUN_POST_CHECKOUT_HOOK; + ropts.head_msg = buf.buf; + if (reset_head(the_repository, &ropts) < 0) ret = error(_("could not switch to %s"), options->switch_to); strbuf_release(&buf); @@ -1033,6 +1041,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) int reschedule_failed_exec = -1; int allow_preemptive_ff = 1; int preserve_merges_selected = 0; + struct reset_head_opts ropts = { 0 }; struct option builtin_rebase_options[] = { OPT_STRING(0, "onto", &options.onto_name, N_("revision"), @@ -1270,9 +1279,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) rerere_clear(the_repository, &merge_rr); string_list_clear(&merge_rr, 1); - - if (reset_head(the_repository, NULL, NULL, RESET_HEAD_HARD, - NULL, NULL, NULL) < 0) + ropts.flags = RESET_HEAD_HARD; + if (reset_head(the_repository, &ropts) < 0) die(_("could not discard worktree changes")); remove_branch_state(the_repository, 0); if (read_basic_state(&options)) @@ -1289,9 +1297,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) if (read_basic_state(&options)) exit(1); - if (reset_head(the_repository, &options.orig_head, - options.head_name, RESET_HEAD_HARD, - NULL, NULL, DEFAULT_REFLOG_ACTION) < 0) + ropts.oid = &options.orig_head; + ropts.branch = options.head_name; + ropts.flags = RESET_HEAD_HARD; + ropts.default_reflog_action = DEFAULT_REFLOG_ACTION; + if (reset_head(the_repository, &ropts) < 0) die(_("could not move back to %s"), oid_to_hex(&options.orig_head)); remove_branch_state(the_repository, 0); @@ -1758,10 +1768,12 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) strbuf_addf(&msg, "%s: checkout %s", getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options.onto_name); - if (reset_head(the_repository, &options.onto->object.oid, NULL, - RESET_HEAD_DETACH | RESET_ORIG_HEAD | - RESET_HEAD_RUN_POST_CHECKOUT_HOOK, - NULL, msg.buf, DEFAULT_REFLOG_ACTION)) + ropts.oid = &options.onto->object.oid; + ropts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD | + RESET_HEAD_RUN_POST_CHECKOUT_HOOK; + ropts.head_msg = msg.buf; + ropts.default_reflog_action = DEFAULT_REFLOG_ACTION; + if (reset_head(the_repository, &ropts)) die(_("Could not detach HEAD")); strbuf_release(&msg); @@ -1776,8 +1788,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) strbuf_addf(&msg, "rebase finished: %s onto %s", options.head_name ? options.head_name : "detached HEAD", oid_to_hex(&options.onto->object.oid)); - reset_head(the_repository, NULL, options.head_name, - RESET_HEAD_REFS_ONLY, NULL, msg.buf, NULL); + memset(&ropts, 0, sizeof(ropts)); + ropts.branch = options.head_name; + ropts.flags = RESET_HEAD_REFS_ONLY; + ropts.head_msg = msg.buf; + reset_head(the_repository, &ropts); strbuf_release(&msg); ret = finish_rebase(&options); goto cleanup; diff --git a/reset.c b/reset.c index 4a92e4bc30..78145d5c45 100644 --- a/reset.c +++ b/reset.c @@ -8,14 +8,17 @@ #include "tree.h" #include "unpack-trees.h" -static int update_refs(const struct object_id *oid, const char *switch_to_branch, - const struct object_id *head, const char *reflog_head, - const char *reflog_orig_head, - const char *default_reflog_action, unsigned flags) +static int update_refs(const struct reset_head_opts *opts, + const struct object_id *oid, + const struct object_id *head) { - unsigned detach_head = flags & RESET_HEAD_DETACH; - unsigned run_hook = flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK; - unsigned update_orig_head = flags & RESET_ORIG_HEAD; + unsigned detach_head = opts->flags & RESET_HEAD_DETACH; + unsigned run_hook = opts->flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK; + unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD; + const char *switch_to_branch = opts->branch; + const char *reflog_head = opts->head_msg; + const char *reflog_orig_head = opts->orig_head_msg; + const char *default_reflog_action = opts->default_reflog_action; struct object_id *old_orig = NULL, oid_old_orig; struct strbuf msg = STRBUF_INIT; const char *reflog_action; @@ -69,14 +72,13 @@ static int update_refs(const struct object_id *oid, const char *switch_to_branch return ret; } -int reset_head(struct repository *r, struct object_id *oid, - const char *switch_to_branch, unsigned flags, - const char *reflog_orig_head, const char *reflog_head, - const char *default_reflog_action) +int reset_head(struct repository *r, const struct reset_head_opts *opts) { - unsigned reset_hard = flags & RESET_HEAD_HARD; - unsigned refs_only = flags & RESET_HEAD_REFS_ONLY; - unsigned update_orig_head = flags & RESET_ORIG_HEAD; + const struct object_id *oid = opts->oid; + const char *switch_to_branch = opts->branch; + unsigned reset_hard = opts->flags & RESET_HEAD_HARD; + unsigned refs_only = opts->flags & RESET_HEAD_REFS_ONLY; + unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD; struct object_id *head = NULL, head_oid; struct tree_desc desc[2] = { { NULL }, { NULL } }; struct lock_file lock = LOCK_INIT; @@ -104,9 +106,7 @@ int reset_head(struct repository *r, struct object_id *oid, oid = &head_oid; if (refs_only) - return update_refs(oid, switch_to_branch, head, reflog_head, - reflog_orig_head, default_reflog_action, - flags); + return update_refs(opts, oid, head); action = reset_hard ? "reset" : "checkout"; setup_unpack_trees_porcelain(&unpack_tree_opts, action); @@ -151,9 +151,7 @@ int reset_head(struct repository *r, struct object_id *oid, } if (oid != &head_oid || update_orig_head || switch_to_branch) - ret = update_refs(oid, switch_to_branch, head, reflog_head, - reflog_orig_head, default_reflog_action, - flags); + ret = update_refs(opts, oid, head); leave_reset_head: rollback_lock_file(&lock); diff --git a/reset.h b/reset.h index 2daec80425..a205be2fb8 100644 --- a/reset.h +++ b/reset.h @@ -6,15 +6,47 @@ #define GIT_REFLOG_ACTION_ENVIRONMENT "GIT_REFLOG_ACTION" +/* Request a detached checkout */ #define RESET_HEAD_DETACH (1<<0) +/* Request a reset rather than a checkout */ #define RESET_HEAD_HARD (1<<1) +/* Run the post-checkout hook */ #define RESET_HEAD_RUN_POST_CHECKOUT_HOOK (1<<2) +/* Only update refs, do not touch the worktree */ #define RESET_HEAD_REFS_ONLY (1<<3) +/* Update ORIG_HEAD as well as HEAD */ #define RESET_ORIG_HEAD (1<<4) -int reset_head(struct repository *r, struct object_id *oid, - const char *switch_to_branch, unsigned flags, - const char *reflog_orig_head, const char *reflog_head, - const char *default_reflog_action); +struct reset_head_opts { + /* + * The commit to checkout/reset to. Defaults to HEAD. + */ + const struct object_id *oid; + /* + * Optional branch to switch to. + */ + const char *branch; + /* + * Flags defined above. + */ + unsigned flags; + /* + * Optional reflog message for HEAD, if this omitted but oid or branch + * are given then default_reflog_action must be given. + */ + const char *head_msg; + /* + * Optional reflog message for ORIG_HEAD, if this omitted and flags + * contains RESET_ORIG_HEAD then default_reflog_action must be given. + */ + const char *orig_head_msg; + /* + * Action to use in default reflog messages, only required if a ref is + * being updated and the reflog messages above are omitted. + */ + const char *default_reflog_action; +}; + +int reset_head(struct repository *r, const struct reset_head_opts *opts); #endif diff --git a/sequencer.c b/sequencer.c index 119564f435..55ed074ae7 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4115,6 +4115,7 @@ void create_autostash(struct repository *r, const char *path) if (has_unstaged_changes(r, 1) || has_uncommitted_changes(r, 1)) { struct child_process stash = CHILD_PROCESS_INIT; + struct reset_head_opts ropts = { .flags = RESET_HEAD_HARD }; struct object_id oid; strvec_pushl(&stash.args, @@ -4136,10 +4137,8 @@ void create_autostash(struct repository *r, const char *path) path); write_file(path, "%s", oid_to_hex(&oid)); printf(_("Created autostash: %s\n"), buf.buf); - if (reset_head(r, NULL, NULL, RESET_HEAD_HARD, NULL, NULL, - NULL) < 0) + if (reset_head(r, &ropts) < 0) die(_("could not reset --hard")); - if (discard_index(r->index) < 0 || repo_read_index(r) < 0) die(_("could not read index")); From 7700ab087b82f71d19134141045b95063e407344 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:47 +0000 Subject: [PATCH 12/14] rebase --apply: fix reflog move_to_original_branch() passes the message intended for the branch reflog as `orig_head_msg`. Fix this by adding a `branch_msg` member to struct reset_head_opts and add a regression test. Note that these reflog messages do not respect GIT_REFLOG_ACTION. They are not alone in that and will be fixed in a future series. The "merge" backend already has tests that check both the branch and HEAD reflogs. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/rebase.c | 8 ++++---- reset.c | 12 ++++++++++-- reset.h | 4 ++++ t/t3406-rebase-message.sh | 23 +++++++++++++++++++++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index ecc368dd4f..b55a9cff05 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -570,7 +570,7 @@ static int finish_rebase(struct rebase_options *opts) static int move_to_original_branch(struct rebase_options *opts) { - struct strbuf orig_head_reflog = STRBUF_INIT, head_reflog = STRBUF_INIT; + struct strbuf branch_reflog = STRBUF_INIT, head_reflog = STRBUF_INIT; struct reset_head_opts ropts = { 0 }; int ret; @@ -580,17 +580,17 @@ static int move_to_original_branch(struct rebase_options *opts) if (!opts->onto) BUG("move_to_original_branch without onto"); - strbuf_addf(&orig_head_reflog, "rebase finished: %s onto %s", + strbuf_addf(&branch_reflog, "rebase finished: %s onto %s", opts->head_name, oid_to_hex(&opts->onto->object.oid)); strbuf_addf(&head_reflog, "rebase finished: returning to %s", opts->head_name); ropts.branch = opts->head_name; ropts.flags = RESET_HEAD_REFS_ONLY; - ropts.orig_head_msg = orig_head_reflog.buf; + ropts.branch_msg = branch_reflog.buf; ropts.head_msg = head_reflog.buf; ret = reset_head(the_repository, &ropts); - strbuf_release(&orig_head_reflog); + strbuf_release(&branch_reflog); strbuf_release(&head_reflog); return ret; } diff --git a/reset.c b/reset.c index 78145d5c45..e02915c0f6 100644 --- a/reset.c +++ b/reset.c @@ -16,6 +16,7 @@ static int update_refs(const struct reset_head_opts *opts, unsigned run_hook = opts->flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK; unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD; const char *switch_to_branch = opts->branch; + const char *reflog_branch = opts->branch_msg; const char *reflog_head = opts->head_msg; const char *reflog_orig_head = opts->orig_head_msg; const char *default_reflog_action = opts->default_reflog_action; @@ -58,8 +59,9 @@ static int update_refs(const struct reset_head_opts *opts, detach_head ? REF_NO_DEREF : 0, UPDATE_REFS_MSG_ON_ERR); else { - ret = update_ref(reflog_head, switch_to_branch, oid, - NULL, 0, UPDATE_REFS_MSG_ON_ERR); + ret = update_ref(reflog_branch ? reflog_branch : reflog_head, + switch_to_branch, oid, NULL, 0, + UPDATE_REFS_MSG_ON_ERR); if (!ret) ret = create_symref("HEAD", switch_to_branch, reflog_head); @@ -90,6 +92,12 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts) if (switch_to_branch && !starts_with(switch_to_branch, "refs/")) BUG("Not a fully qualified branch: '%s'", switch_to_branch); + if (opts->orig_head_msg && !update_orig_head) + BUG("ORIG_HEAD reflog message given without updating ORIG_HEAD"); + + if (opts->branch_msg && !opts->branch) + BUG("branch reflog message given without a branch"); + if (!refs_only && repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) { ret = -1; goto leave_reset_head; diff --git a/reset.h b/reset.h index a205be2fb8..7ef7e43ea8 100644 --- a/reset.h +++ b/reset.h @@ -30,6 +30,10 @@ struct reset_head_opts { * Flags defined above. */ unsigned flags; + /* + * Optional reflog message for branch, defaults to head_msg. + */ + const char *branch_msg; /* * Optional reflog message for HEAD, if this omitted but oid or branch * are given then default_reflog_action must be given. diff --git a/t/t3406-rebase-message.sh b/t/t3406-rebase-message.sh index 77a313f62e..d17b450e81 100755 --- a/t/t3406-rebase-message.sh +++ b/t/t3406-rebase-message.sh @@ -105,6 +105,29 @@ test_expect_success 'GIT_REFLOG_ACTION' ' test_cmp expect actual ' +test_expect_success 'rebase --apply reflog' ' + git checkout -b reflog-apply start && + old_head_reflog="$(git log -g --format=%gs -1 HEAD)" && + + git rebase --apply Y && + + git log -g --format=%gs -4 HEAD >actual && + cat >expect <<-EOF && + rebase finished: returning to refs/heads/reflog-apply + rebase: Z + rebase: checkout Y + $old_head_reflog + EOF + test_cmp expect actual && + + git log -g --format=%gs -2 reflog-apply >actual && + cat >expect <<-EOF && + rebase finished: refs/heads/reflog-apply onto $(git rev-parse Y) + branch: Created from start + EOF + test_cmp expect actual +' + test_expect_success 'rebase -i onto unrelated history' ' git init unrelated && test_commit -C unrelated 1 && From cd1528ef8ef9847fc27cff4016bf073f04729504 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:48 +0000 Subject: [PATCH 13/14] rebase --apply: set ORIG_HEAD correctly At the start of a rebase, ORIG_HEAD is updated to the tip of the branch being rebased. Unfortunately reset_head() always uses the current value of HEAD for this which is incorrect if the rebase is started with "git rebase " as in that case ORIG_HEAD should be updated to . This only affects the "apply" backend as the "merge" backend does not yet use reset_head() for the initial checkout. Fix this by passing in orig_head when calling reset_head() and add some regression tests. Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- builtin/rebase.c | 1 + reset.c | 4 +++- reset.h | 4 ++++ t/t3418-rebase-continue.sh | 26 ++++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/builtin/rebase.c b/builtin/rebase.c index b55a9cff05..e942c300f8 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -1769,6 +1769,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) strbuf_addf(&msg, "%s: checkout %s", getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options.onto_name); ropts.oid = &options.onto->object.oid; + ropts.orig_head = &options.orig_head, ropts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD | RESET_HEAD_RUN_POST_CHECKOUT_HOOK; ropts.head_msg = msg.buf; diff --git a/reset.c b/reset.c index e02915c0f6..448cb3fd78 100644 --- a/reset.c +++ b/reset.c @@ -15,6 +15,7 @@ static int update_refs(const struct reset_head_opts *opts, unsigned detach_head = opts->flags & RESET_HEAD_DETACH; unsigned run_hook = opts->flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK; unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD; + const struct object_id *orig_head = opts->orig_head; const char *switch_to_branch = opts->branch; const char *reflog_branch = opts->branch_msg; const char *reflog_head = opts->head_msg; @@ -43,7 +44,8 @@ static int update_refs(const struct reset_head_opts *opts, strbuf_addstr(&msg, "updating ORIG_HEAD"); reflog_orig_head = msg.buf; } - update_ref(reflog_orig_head, "ORIG_HEAD", head, + update_ref(reflog_orig_head, "ORIG_HEAD", + orig_head ? orig_head : head, old_orig, 0, UPDATE_REFS_MSG_ON_ERR); } else if (old_orig) delete_ref(NULL, "ORIG_HEAD", old_orig, 0); diff --git a/reset.h b/reset.h index 7ef7e43ea8..a28f81829d 100644 --- a/reset.h +++ b/reset.h @@ -22,6 +22,10 @@ struct reset_head_opts { * The commit to checkout/reset to. Defaults to HEAD. */ const struct object_id *oid; + /* + * Optional value to set ORIG_HEAD. Defaults to HEAD. + */ + const struct object_id *orig_head; /* * Optional branch to switch to. */ diff --git a/t/t3418-rebase-continue.sh b/t/t3418-rebase-continue.sh index 22eca73aa3..130e2f9b55 100755 --- a/t/t3418-rebase-continue.sh +++ b/t/t3418-rebase-continue.sh @@ -308,4 +308,30 @@ test_expect_success 'there is no --no-reschedule-failed-exec in an ongoing rebas test_expect_code 129 git rebase --edit-todo --no-reschedule-failed-exec ' +test_orig_head_helper () { + test_when_finished 'git rebase --abort && + git checkout topic && + git reset --hard commit-new-file-F2-on-topic-branch' && + git update-ref -d ORIG_HEAD && + test_must_fail git rebase "$@" && + test_cmp_rev ORIG_HEAD commit-new-file-F2-on-topic-branch +} + +test_orig_head () { + type=$1 + test_expect_success "rebase $type sets ORIG_HEAD correctly" ' + git checkout topic && + git reset --hard commit-new-file-F2-on-topic-branch && + test_orig_head_helper $type main + ' + + test_expect_success "rebase $type sets ORIG_HEAD correctly" ' + git checkout main && + test_orig_head_helper $type main topic + ' +} + +test_orig_head --apply +test_orig_head --merge + test_done From 38c541ce94048cf72aa4f465be9314423a57f445 Mon Sep 17 00:00:00 2001 From: Phillip Wood Date: Wed, 26 Jan 2022 13:05:49 +0000 Subject: [PATCH 14/14] rebase -m: don't fork git checkout Now that reset_head() can handle the initial checkout of onto correctly use it in the "merge" backend instead of forking "git checkout". This opens the way for us to stop calling the post-checkout hook in the future. Not running "git checkout" means that "rebase -i/m" no longer recurse submodules when checking out "onto" (thanks to Philippe Blain for pointing this out). As the rest of rebase does not know what to do with submodules this is probably a good thing. When using merge-ort rebase ought be able to handle submodules correctly if it parsed the submodule config, such a change is left for a future patch series. The "apply" based rebase has avoided forking git checkout since ac7f467fef ("builtin/rebase: support running "git rebase "", 2018-08-07). The code that handles the checkout was moved into libgit by b309a97108 ("reset: extract reset_head() from rebase", 2020-04-07). Signed-off-by: Phillip Wood Signed-off-by: Junio C Hamano --- sequencer.c | 38 +++++++++++--------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/sequencer.c b/sequencer.c index 55ed074ae7..bdd66b4b67 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4223,42 +4223,26 @@ int apply_autostash_oid(const char *stash_oid) return apply_save_autostash_oid(stash_oid, 1); } -static int run_git_checkout(struct repository *r, struct replay_opts *opts, - const char *commit, const char *action) -{ - struct child_process cmd = CHILD_PROCESS_INIT; - int ret; - - cmd.git_cmd = 1; - - strvec_push(&cmd.args, "checkout"); - strvec_push(&cmd.args, commit); - strvec_pushf(&cmd.env_array, GIT_REFLOG_ACTION "=%s", action); - - if (opts->verbose) - ret = run_command(&cmd); - else - ret = run_command_silent_on_success(&cmd); - - if (!ret) - discard_index(r->index); - - return ret; -} - static int checkout_onto(struct repository *r, struct replay_opts *opts, const char *onto_name, const struct object_id *onto, const struct object_id *orig_head) { - const char *action = reflog_message(opts, "start", "checkout %s", onto_name); - - if (run_git_checkout(r, opts, oid_to_hex(onto), action)) { + struct reset_head_opts ropts = { + .oid = onto, + .orig_head = orig_head, + .flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD | + RESET_HEAD_RUN_POST_CHECKOUT_HOOK, + .head_msg = reflog_message(opts, "start", "checkout %s", + onto_name), + .default_reflog_action = "rebase" + }; + if (reset_head(r, &ropts)) { apply_autostash(rebase_path_autostash()); sequencer_remove_state(opts); return error(_("could not detach HEAD")); } - return update_ref(NULL, "ORIG_HEAD", orig_head, NULL, 0, UPDATE_REFS_MSG_ON_ERR); + return 0; } static int stopped_at_head(struct repository *r)