Re: [PATCH v3 6/8] rebase: add --update-refs option

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Tue, Jun 28, 2022 at 6:26 AM Derrick Stolee via GitGitGadget
<gitgitgadget@xxxxxxxxx> wrote:
>
> From: Derrick Stolee <derrickstolee@xxxxxxxxxx>
>
> When working on a large feature, it can be helpful to break that feature
> into multiple smaller parts that become reviewed in sequence. During
> development or during review, a change to one part of the feature could
> affect multiple of these parts. An interactive rebase can help adjust
> the multi-part "story" of the branch.
>
> However, if there are branches tracking the different parts of the
> feature, then rebasing the entire list of commits can create commits not
> reachable from those "sub branches". It can take a manual step to update
> those branches.
>
> Add a new --update-refs option to 'git rebase -i' that adds 'update-ref
> <ref>' steps to the todo file whenever a commit that is being rebased is
> decorated with that <ref>. At the very end, the rebase process updates
> all of the listed refs to the values stored during the rebase operation.
>
> Be sure to iterate after any squashing or fixups are placed. Update the
> branch only after those squashes and fixups are complete. This allows a
> --fixup commit at the tip of the feature to apply correctly to the sub
> branch, even if it is fixing up the most-recent commit in that part.
>
> One potential problem here is that refs decorating commits that are
> already marked as "fixup!" or "squash!" will not be included in this
> list. Generally, the reordering of the "fixup!" and "squash!" is likely
> to change the relative order of these refs, so it is not recommended.
> The workflow here is intended to allow these kinds of commits at the tip
> of the rebased branch while the other sub branches come along for the
> ride without intervention.
>
> This change update the documentation and builtin to accept the
> --update-refs option as well as updating the todo file with the
> 'update-ref' commands. Tests are added to ensure that these todo
> commands are added in the correct locations.
>
> A future change will update the behavior to actually update the refs
> at the end of the rebase sequence.
>
> Signed-off-by: Derrick Stolee <derrickstolee@xxxxxxxxxx>
> ---
>  Documentation/git-rebase.txt  |   8 +++
>  builtin/rebase.c              |   5 ++
>  sequencer.c                   | 107 ++++++++++++++++++++++++++++++++++
>  sequencer.h                   |   1 +
>  t/t2407-worktree-heads.sh     |  17 ++++++
>  t/t3404-rebase-interactive.sh |  70 ++++++++++++++++++++++
>  6 files changed, 208 insertions(+)
>
> diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
> index 262fb01aec0..e7611b4089c 100644
> --- a/Documentation/git-rebase.txt
> +++ b/Documentation/git-rebase.txt
> @@ -609,6 +609,13 @@ provided. Otherwise an explicit `--no-reschedule-failed-exec` at the
>  start would be overridden by the presence of
>  `rebase.rescheduleFailedExec=true` configuration.
>
> +--update-refs::
> +--no-update-refs::
> +       Automatically force-update any branches that point to commits that
> +       are being rebased. Any branches that are checked out in a worktree
> +       or point to a `squash! ...` or `fixup! ...` commit are not updated
> +       in this way.
> +
>  INCOMPATIBLE OPTIONS
>  --------------------
>
> @@ -632,6 +639,7 @@ are incompatible with the following options:
>   * --empty=
>   * --reapply-cherry-picks
>   * --edit-todo
> + * --update-refs
>   * --root when used in combination with --onto
>
>  In addition, the following pairs of options are incompatible:
> diff --git a/builtin/rebase.c b/builtin/rebase.c
> index 7ab50cda2ad..56d82a52106 100644
> --- a/builtin/rebase.c
> +++ b/builtin/rebase.c
> @@ -102,6 +102,7 @@ struct rebase_options {
>         int reschedule_failed_exec;
>         int reapply_cherry_picks;
>         int fork_point;
> +       int update_refs;
>  };
>
>  #define REBASE_OPTIONS_INIT {                          \
> @@ -298,6 +299,7 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)
>                 ret = complete_action(the_repository, &replay, flags,
>                         shortrevisions, opts->onto_name, opts->onto,
>                         &opts->orig_head, &commands, opts->autosquash,
> +                       opts->update_refs,
>                         &todo_list);
>         }
>
> @@ -1124,6 +1126,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
>                 OPT_BOOL(0, "autosquash", &options.autosquash,
>                          N_("move commits that begin with "
>                             "squash!/fixup! under -i")),
> +               OPT_BOOL(0, "update-refs", &options.update_refs,
> +                        N_("update local refs that point to commits "
> +                           "that are being rebased")),
>                 { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"),
>                         N_("GPG-sign commits"),
>                         PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
> diff --git a/sequencer.c b/sequencer.c
> index 0b61835d441..915d87a0336 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -35,6 +35,7 @@
>  #include "commit-reach.h"
>  #include "rebase-interactive.h"
>  #include "reset.h"
> +#include "branch.h"
>
>  #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
>
> @@ -5615,10 +5616,113 @@ static int skip_unnecessary_picks(struct repository *r,
>         return 0;
>  }
>
> +struct todo_add_branch_context {
> +       struct todo_item *items;
> +       size_t items_nr;
> +       size_t items_alloc;
> +       struct strbuf *buf;
> +       struct commit *commit;
> +       struct string_list refs_to_oids;
> +};
> +
> +static int add_decorations_to_list(const struct commit *commit,
> +                                  struct todo_add_branch_context *ctx)
> +{
> +       const struct name_decoration *decoration = get_name_decoration(&commit->object);
> +
> +       while (decoration) {
> +               struct todo_item *item;
> +               const char *path;
> +               size_t base_offset = ctx->buf->len;
> +
> +               ALLOC_GROW(ctx->items,
> +                       ctx->items_nr + 1,
> +                       ctx->items_alloc);
> +               item = &ctx->items[ctx->items_nr];
> +               memset(item, 0, sizeof(*item));
> +
> +               /* If the branch is checked out, then leave a comment instead. */
> +               if ((path = branch_checked_out(decoration->name))) {
> +                       item->command = TODO_COMMENT;
> +                       strbuf_addf(ctx->buf, "# Ref %s checked out at '%s'\n",
> +                                   decoration->name, path);
> +               } else {
> +                       struct string_list_item *sti;
> +                       item->command = TODO_UPDATE_REF;
> +                       strbuf_addf(ctx->buf, "%s\n", decoration->name);
> +
> +                       sti = string_list_append(&ctx->refs_to_oids,
> +                                                decoration->name);
> +                       sti->util = oiddup(the_hash_algo->null_oid);
> +               }
> +
> +               item->offset_in_buf = base_offset;
> +               item->arg_offset = base_offset;
> +               item->arg_len = ctx->buf->len - base_offset;
> +               ctx->items_nr++;
> +
> +               decoration = decoration->next;
> +       }
> +
> +       return 0;
> +}
> +
> +/*
> + * For each 'pick' command, find out if the commit has a decoration in

Is this really limited to picks?  If someone uses --autosquash and has
a fixup or squash in the list, wouldn't this apply as well, or does
all of this apply before the transformations to fixup/squash?  Also,
what if the user is doing --rebase-merges and there's a merge commit
with a branch pointing at the merge.   Would that be included?

> + * refs/heads/. If so, then add a 'label for-update-refs/' command.
> + */
> +static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
> +{
> +       int i;
> +       static struct string_list decorate_refs_exclude = STRING_LIST_INIT_NODUP;
> +       static struct string_list decorate_refs_exclude_config = STRING_LIST_INIT_NODUP;
> +       static struct string_list decorate_refs_include = STRING_LIST_INIT_NODUP;
> +       struct decoration_filter decoration_filter = {
> +               .include_ref_pattern = &decorate_refs_include,
> +               .exclude_ref_pattern = &decorate_refs_exclude,
> +               .exclude_ref_config_pattern = &decorate_refs_exclude_config,
> +       };
> +       struct todo_add_branch_context ctx = {
> +               .buf = &todo_list->buf,
> +               .refs_to_oids = STRING_LIST_INIT_DUP,
> +       };
> +
> +       ctx.items_alloc = 2 * todo_list->nr + 1;
> +       ALLOC_ARRAY(ctx.items, ctx.items_alloc);
> +
> +       string_list_append(&decorate_refs_include, "refs/heads/");
> +       load_ref_decorations(&decoration_filter, 0);
> +
> +       for (i = 0; i < todo_list->nr; ) {
> +               struct todo_item *item = &todo_list->items[i];
> +
> +               /* insert ith item into new list */
> +               ALLOC_GROW(ctx.items,
> +                          ctx.items_nr + 1,
> +                          ctx.items_alloc);
> +
> +               ctx.items[ctx.items_nr++] = todo_list->items[i++];
> +
> +               if (item->commit) {
> +                       ctx.commit = item->commit;
> +                       add_decorations_to_list(item->commit, &ctx);
> +               }
> +       }
> +
> +       string_list_clear(&ctx.refs_to_oids, 1);
> +       free(todo_list->items);
> +       todo_list->items = ctx.items;
> +       todo_list->nr = ctx.items_nr;
> +       todo_list->alloc = ctx.items_alloc;
> +
> +       return 0;
> +}
> +
>  int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags,
>                     const char *shortrevisions, const char *onto_name,
>                     struct commit *onto, const struct object_id *orig_head,
>                     struct string_list *commands, unsigned autosquash,
> +                   unsigned update_refs,
>                     struct todo_list *todo_list)
>  {
>         char shortonto[GIT_MAX_HEXSZ + 1];
> @@ -5637,6 +5741,9 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
>                 item->arg_len = item->arg_offset = item->flags = item->offset_in_buf = 0;
>         }
>
> +       if (update_refs && todo_list_add_update_ref_commands(todo_list))
> +               return -1;
> +
>         if (autosquash && todo_list_rearrange_squash(todo_list))
>                 return -1;
>
> diff --git a/sequencer.h b/sequencer.h
> index 2cf5c1b9a38..e671d7e0743 100644
> --- a/sequencer.h
> +++ b/sequencer.h
> @@ -167,6 +167,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
>                     const char *shortrevisions, const char *onto_name,
>                     struct commit *onto, const struct object_id *orig_head,
>                     struct string_list *commands, unsigned autosquash,
> +                   unsigned update_refs,
>                     struct todo_list *todo_list);
>  int todo_list_rearrange_squash(struct todo_list *todo_list);
>
> diff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh
> index f1e9e172a0c..203997d92c6 100755
> --- a/t/t2407-worktree-heads.sh
> +++ b/t/t2407-worktree-heads.sh
> @@ -151,4 +151,21 @@ test_expect_success 'refuse to overwrite when in error states' '
>         done
>  '
>
> +. "$TEST_DIRECTORY"/lib-rebase.sh
> +
> +test_expect_success !SANITIZE_LEAK 'refuse to overwrite during rebase with --update-refs' '
> +       git commit --fixup HEAD~2 --allow-empty &&
> +       (
> +               set_cat_todo_editor &&
> +               test_must_fail git rebase -i --update-refs HEAD~3 >todo &&
> +               ! grep "update-refs" todo
> +       ) &&
> +       git branch -f allow-update HEAD~2 &&
> +       (
> +               set_cat_todo_editor &&
> +               test_must_fail git rebase -i --update-refs HEAD~3 >todo &&
> +               grep "update-ref refs/heads/allow-update" todo
> +       )
> +'
> +
>  test_done
> diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
> index f31afd4a547..3cd20733bc8 100755
> --- a/t/t3404-rebase-interactive.sh
> +++ b/t/t3404-rebase-interactive.sh
> @@ -1743,6 +1743,76 @@ test_expect_success 'ORIG_HEAD is updated correctly' '
>         test_cmp_rev ORIG_HEAD test-orig-head@{1}
>  '
>
> +test_expect_success '--update-refs adds label and update-ref commands' '
> +       git checkout -b update-refs no-conflict-branch &&
> +       git branch -f base HEAD~4 &&
> +       git branch -f first HEAD~3 &&
> +       git branch -f second HEAD~3 &&
> +       git branch -f third HEAD~1 &&
> +       git commit --allow-empty --fixup=third &&
> +       git branch -f shared-tip &&
> +       (
> +               set_cat_todo_editor &&
> +
> +               cat >expect <<-EOF &&
> +               pick $(git log -1 --format=%h J) J
> +               update-ref refs/heads/second
> +               update-ref refs/heads/first
> +               pick $(git log -1 --format=%h K) K
> +               pick $(git log -1 --format=%h L) L
> +               fixup $(git log -1 --format=%h update-refs) fixup! L # empty
> +               update-ref refs/heads/third
> +               pick $(git log -1 --format=%h M) M
> +               update-ref refs/heads/no-conflict-branch
> +               update-ref refs/heads/shared-tip
> +               EOF
> +
> +               test_must_fail git rebase -i --autosquash --update-refs primary >todo &&
> +               test_cmp expect todo
> +       )
> +'
> +
> +test_expect_success '--update-refs adds commands with --rebase-merges' '
> +       git checkout -b update-refs-with-merge no-conflict-branch &&
> +       git branch -f base HEAD~4 &&
> +       git branch -f first HEAD~3 &&
> +       git branch -f second HEAD~3 &&
> +       git branch -f third HEAD~1 &&
> +       git merge -m merge branch2 &&
> +       git branch -f merge-branch &&
> +       git commit --fixup=third --allow-empty &&
> +       (
> +               set_cat_todo_editor &&
> +
> +               cat >expect <<-EOF &&
> +               label onto
> +               reset onto
> +               pick $(git log -1 --format=%h branch2~1) F
> +               pick $(git log -1 --format=%h branch2) I
> +               update-ref refs/heads/branch2
> +               label merge
> +               reset onto
> +               pick $(git log -1 --format=%h refs/heads/second) J
> +               update-ref refs/heads/second
> +               update-ref refs/heads/first
> +               pick $(git log -1 --format=%h refs/heads/third~1) K
> +               pick $(git log -1 --format=%h refs/heads/third) L
> +               fixup $(git log -1 --format=%h update-refs-with-merge) fixup! L # empty
> +               update-ref refs/heads/third
> +               pick $(git log -1 --format=%h HEAD~2) M
> +               update-ref refs/heads/no-conflict-branch
> +               merge -C $(git log -1 --format=%h HEAD~1) merge # merge
> +               update-ref refs/heads/merge-branch
> +               EOF
> +
> +               test_must_fail git rebase -i --autosquash \
> +                                  --rebase-merges=rebase-cousins \
> +                                  --update-refs primary >todo &&
> +
> +               test_cmp expect todo
> +       )
> +'
> +
>  # This must be the last test in this file
>  test_expect_success '$EDITOR and friends are unchanged' '
>         test_editor_unchanged
> --
> gitgitgadget
>



[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux