Skip to content

Commit 900b50c

Browse files
derrickstoleegitster
authored andcommitted
rebase: add --update-refs option
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. 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. This change does _not_ include the actual behavior of tracking the updated refs and writing the new ref values at the end of the rebase process. That is deferred to a later change. Signed-off-by: Derrick Stolee <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent a97d791 commit 900b50c

File tree

6 files changed

+216
-0
lines changed

6 files changed

+216
-0
lines changed

Documentation/git-rebase.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,12 @@ provided. Otherwise an explicit `--no-reschedule-failed-exec` at the
609609
start would be overridden by the presence of
610610
`rebase.rescheduleFailedExec=true` configuration.
611611

612+
--update-refs::
613+
--no-update-refs::
614+
Automatically force-update any branches that point to commits that
615+
are being rebased. Any branches that are checked out in a worktree
616+
are not updated in this way.
617+
612618
INCOMPATIBLE OPTIONS
613619
--------------------
614620

@@ -632,6 +638,7 @@ are incompatible with the following options:
632638
* --empty=
633639
* --reapply-cherry-picks
634640
* --edit-todo
641+
* --update-refs
635642
* --root when used in combination with --onto
636643

637644
In addition, the following pairs of options are incompatible:

builtin/rebase.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ struct rebase_options {
102102
int reschedule_failed_exec;
103103
int reapply_cherry_picks;
104104
int fork_point;
105+
int update_refs;
105106
};
106107

107108
#define REBASE_OPTIONS_INIT { \
@@ -298,6 +299,7 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags)
298299
ret = complete_action(the_repository, &replay, flags,
299300
shortrevisions, opts->onto_name, opts->onto,
300301
&opts->orig_head, &commands, opts->autosquash,
302+
opts->update_refs,
301303
&todo_list);
302304
}
303305

@@ -1124,6 +1126,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
11241126
OPT_BOOL(0, "autosquash", &options.autosquash,
11251127
N_("move commits that begin with "
11261128
"squash!/fixup! under -i")),
1129+
OPT_BOOL(0, "update-refs", &options.update_refs,
1130+
N_("update branches that point to commits "
1131+
"that are being rebased")),
11271132
{ OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"),
11281133
N_("GPG-sign commits"),
11291134
PARSE_OPT_OPTARG, NULL, (intptr_t) "" },

sequencer.c

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "commit-reach.h"
3636
#include "rebase-interactive.h"
3737
#include "reset.h"
38+
#include "branch.h"
3839

3940
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
4041

@@ -5673,10 +5674,113 @@ static int skip_unnecessary_picks(struct repository *r,
56735674
return 0;
56745675
}
56755676

5677+
struct todo_add_branch_context {
5678+
struct todo_item *items;
5679+
size_t items_nr;
5680+
size_t items_alloc;
5681+
struct strbuf *buf;
5682+
struct commit *commit;
5683+
struct string_list refs_to_oids;
5684+
};
5685+
5686+
static int add_decorations_to_list(const struct commit *commit,
5687+
struct todo_add_branch_context *ctx)
5688+
{
5689+
const struct name_decoration *decoration = get_name_decoration(&commit->object);
5690+
5691+
while (decoration) {
5692+
struct todo_item *item;
5693+
const char *path;
5694+
size_t base_offset = ctx->buf->len;
5695+
5696+
ALLOC_GROW(ctx->items,
5697+
ctx->items_nr + 1,
5698+
ctx->items_alloc);
5699+
item = &ctx->items[ctx->items_nr];
5700+
memset(item, 0, sizeof(*item));
5701+
5702+
/* If the branch is checked out, then leave a comment instead. */
5703+
if ((path = branch_checked_out(decoration->name))) {
5704+
item->command = TODO_COMMENT;
5705+
strbuf_addf(ctx->buf, "# Ref %s checked out at '%s'\n",
5706+
decoration->name, path);
5707+
} else {
5708+
struct string_list_item *sti;
5709+
item->command = TODO_UPDATE_REF;
5710+
strbuf_addf(ctx->buf, "%s\n", decoration->name);
5711+
5712+
sti = string_list_insert(&ctx->refs_to_oids,
5713+
decoration->name);
5714+
sti->util = oiddup(the_hash_algo->null_oid);
5715+
}
5716+
5717+
item->offset_in_buf = base_offset;
5718+
item->arg_offset = base_offset;
5719+
item->arg_len = ctx->buf->len - base_offset;
5720+
ctx->items_nr++;
5721+
5722+
decoration = decoration->next;
5723+
}
5724+
5725+
return 0;
5726+
}
5727+
5728+
/*
5729+
* For each 'pick' command, find out if the commit has a decoration in
5730+
* refs/heads/. If so, then add a 'label for-update-refs/' command.
5731+
*/
5732+
static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
5733+
{
5734+
int i;
5735+
static struct string_list decorate_refs_exclude = STRING_LIST_INIT_NODUP;
5736+
static struct string_list decorate_refs_exclude_config = STRING_LIST_INIT_NODUP;
5737+
static struct string_list decorate_refs_include = STRING_LIST_INIT_NODUP;
5738+
struct decoration_filter decoration_filter = {
5739+
.include_ref_pattern = &decorate_refs_include,
5740+
.exclude_ref_pattern = &decorate_refs_exclude,
5741+
.exclude_ref_config_pattern = &decorate_refs_exclude_config,
5742+
};
5743+
struct todo_add_branch_context ctx = {
5744+
.buf = &todo_list->buf,
5745+
.refs_to_oids = STRING_LIST_INIT_DUP,
5746+
};
5747+
5748+
ctx.items_alloc = 2 * todo_list->nr + 1;
5749+
ALLOC_ARRAY(ctx.items, ctx.items_alloc);
5750+
5751+
string_list_append(&decorate_refs_include, "refs/heads/");
5752+
load_ref_decorations(&decoration_filter, 0);
5753+
5754+
for (i = 0; i < todo_list->nr; ) {
5755+
struct todo_item *item = &todo_list->items[i];
5756+
5757+
/* insert ith item into new list */
5758+
ALLOC_GROW(ctx.items,
5759+
ctx.items_nr + 1,
5760+
ctx.items_alloc);
5761+
5762+
ctx.items[ctx.items_nr++] = todo_list->items[i++];
5763+
5764+
if (item->commit) {
5765+
ctx.commit = item->commit;
5766+
add_decorations_to_list(item->commit, &ctx);
5767+
}
5768+
}
5769+
5770+
string_list_clear(&ctx.refs_to_oids, 1);
5771+
free(todo_list->items);
5772+
todo_list->items = ctx.items;
5773+
todo_list->nr = ctx.items_nr;
5774+
todo_list->alloc = ctx.items_alloc;
5775+
5776+
return 0;
5777+
}
5778+
56765779
int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags,
56775780
const char *shortrevisions, const char *onto_name,
56785781
struct commit *onto, const struct object_id *orig_head,
56795782
struct string_list *commands, unsigned autosquash,
5783+
unsigned update_refs,
56805784
struct todo_list *todo_list)
56815785
{
56825786
char shortonto[GIT_MAX_HEXSZ + 1];
@@ -5695,6 +5799,9 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
56955799
item->arg_len = item->arg_offset = item->flags = item->offset_in_buf = 0;
56965800
}
56975801

5802+
if (update_refs && todo_list_add_update_ref_commands(todo_list))
5803+
return -1;
5804+
56985805
if (autosquash && todo_list_rearrange_squash(todo_list))
56995806
return -1;
57005807

sequencer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
168168
const char *shortrevisions, const char *onto_name,
169169
struct commit *onto, const struct object_id *orig_head,
170170
struct string_list *commands, unsigned autosquash,
171+
unsigned update_refs,
171172
struct todo_list *todo_list);
172173
int todo_list_rearrange_squash(struct todo_list *todo_list);
173174

t/t2407-worktree-heads.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,26 @@ test_expect_success 'refuse to overwrite when in error states' '
164164
done
165165
'
166166

167+
. "$TEST_DIRECTORY"/lib-rebase.sh
168+
169+
test_expect_success !SANITIZE_LEAK 'refuse to overwrite during rebase with --update-refs' '
170+
git commit --fixup HEAD~2 --allow-empty &&
171+
(
172+
set_cat_todo_editor &&
173+
test_must_fail git rebase -i --update-refs HEAD~3 >todo &&
174+
! grep "update-refs" todo
175+
) &&
176+
git branch -f allow-update HEAD~2 &&
177+
(
178+
set_cat_todo_editor &&
179+
test_must_fail git rebase -i --update-refs HEAD~3 >todo &&
180+
grep "update-ref refs/heads/allow-update" todo
181+
)
182+
'
183+
184+
# This must be the last test in this file
185+
test_expect_success '$EDITOR and friends are unchanged' '
186+
test_editor_unchanged
187+
'
188+
167189
test_done

t/t3404-rebase-interactive.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,80 @@ test_expect_success 'ORIG_HEAD is updated correctly' '
17431743
test_cmp_rev ORIG_HEAD test-orig-head@{1}
17441744
'
17451745

1746+
test_expect_success '--update-refs adds label and update-ref commands' '
1747+
git checkout -b update-refs no-conflict-branch &&
1748+
git branch -f base HEAD~4 &&
1749+
git branch -f first HEAD~3 &&
1750+
git branch -f second HEAD~3 &&
1751+
git branch -f third HEAD~1 &&
1752+
git commit --allow-empty --fixup=third &&
1753+
git branch -f is-not-reordered &&
1754+
git commit --allow-empty --fixup=HEAD~4 &&
1755+
git branch -f shared-tip &&
1756+
(
1757+
set_cat_todo_editor &&
1758+
1759+
cat >expect <<-EOF &&
1760+
pick $(git log -1 --format=%h J) J
1761+
fixup $(git log -1 --format=%h update-refs) fixup! J # empty
1762+
update-ref refs/heads/second
1763+
update-ref refs/heads/first
1764+
pick $(git log -1 --format=%h K) K
1765+
pick $(git log -1 --format=%h L) L
1766+
fixup $(git log -1 --format=%h is-not-reordered) fixup! L # empty
1767+
update-ref refs/heads/third
1768+
pick $(git log -1 --format=%h M) M
1769+
update-ref refs/heads/no-conflict-branch
1770+
update-ref refs/heads/is-not-reordered
1771+
update-ref refs/heads/shared-tip
1772+
EOF
1773+
1774+
test_must_fail git rebase -i --autosquash --update-refs primary >todo &&
1775+
test_cmp expect todo
1776+
)
1777+
'
1778+
1779+
test_expect_success '--update-refs adds commands with --rebase-merges' '
1780+
git checkout -b update-refs-with-merge no-conflict-branch &&
1781+
git branch -f base HEAD~4 &&
1782+
git branch -f first HEAD~3 &&
1783+
git branch -f second HEAD~3 &&
1784+
git branch -f third HEAD~1 &&
1785+
git merge -m merge branch2 &&
1786+
git branch -f merge-branch &&
1787+
git commit --fixup=third --allow-empty &&
1788+
(
1789+
set_cat_todo_editor &&
1790+
1791+
cat >expect <<-EOF &&
1792+
label onto
1793+
reset onto
1794+
pick $(git log -1 --format=%h branch2~1) F
1795+
pick $(git log -1 --format=%h branch2) I
1796+
update-ref refs/heads/branch2
1797+
label merge
1798+
reset onto
1799+
pick $(git log -1 --format=%h refs/heads/second) J
1800+
update-ref refs/heads/second
1801+
update-ref refs/heads/first
1802+
pick $(git log -1 --format=%h refs/heads/third~1) K
1803+
pick $(git log -1 --format=%h refs/heads/third) L
1804+
fixup $(git log -1 --format=%h update-refs-with-merge) fixup! L # empty
1805+
update-ref refs/heads/third
1806+
pick $(git log -1 --format=%h HEAD~2) M
1807+
update-ref refs/heads/no-conflict-branch
1808+
merge -C $(git log -1 --format=%h HEAD~1) merge # merge
1809+
update-ref refs/heads/merge-branch
1810+
EOF
1811+
1812+
test_must_fail git rebase -i --autosquash \
1813+
--rebase-merges=rebase-cousins \
1814+
--update-refs primary >todo &&
1815+
1816+
test_cmp expect todo
1817+
)
1818+
'
1819+
17461820
# This must be the last test in this file
17471821
test_expect_success '$EDITOR and friends are unchanged' '
17481822
test_editor_unchanged

0 commit comments

Comments
 (0)