Skip to content

Commit 5a5445d

Browse files
agrngitster
authored andcommitted
rebase-interactive: warn if commit is dropped with `rebase --edit-todo'
When set to "warn" or "error", `rebase.missingCommitsCheck' would make `rebase -i' warn if the user removed commits from the todo list to prevent mistakes. Unfortunately, `rebase --edit-todo' and `rebase --continue' don't take it into account. This adds the ability for `rebase --edit-todo' and `rebase --continue' to check if commits were dropped by the user. As both edit_todo_list() and complete_action() parse the todo list and check for dropped commits, the code doing so in the latter is removed to reduce duplication. `edit_todo_list_advice' is removed from sequencer.c as it is no longer used there. This changes when a backup of the todo list is made. Until now, it was saved only once, before the initial edit. Now, it is also made if the original todo list has no errors or no dropped commits. Thus, the backup should be error-free. Without this, sequencer_continue() (`rebase --continue') could only compare the current todo list against the original, unedited list. Before this change, this file was only used by edit_todo_list() and `rebase -p' to create the backup before the initial edit, and check_todo_list_from_file(), only used by `rebase -p' to check for dropped commits after its own initial edit. If the edited list has an error, a file, `dropped', is created to report the issue. Otherwise, it is deleted. Usually, the edited list is compared against the list before editing, but if this file exists, it will be compared to the backup. Also, if the file exists, sequencer_continue() checks the list for dropped commits. If the check was performed every time, it would fail when resuming a rebase after resolving a conflict, as the backup will contain commits that were picked, but they will not be in the new list. It's safe to ignore this check if `dropped' does not exist, because that means that no errors were found at the last edition, so any missing commits here have already been picked. Five tests are added to t3404. The tests for `rebase.missingCommitsCheck = warn' and `rebase.missingCommitsCheck = error' have a similar structure. First, we start a rebase with an incorrect command on the first line. Then, we edit the todo list, removing the first and the last lines. This demonstrates that `--edit-todo' notices dropped commits, but not when the command is incorrect. Then, we restore the original todo list, and edit it to remove the last line. This demonstrates that if we add a commit after the initial edit, then remove it, `--edit-todo' will notice that it has been dropped. Then, the actual rebase takes place. In the third test, it is also checked that `--continue' will refuse to resume the rebase if commits were dropped. The fourth test checks that no errors are raised when resuming a rebase after resolving a conflict, the fifth checks that no errors are raised when editing the todo list after pausing the rebase. Signed-off-by: Alban Gruin <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 1da5874 commit 5a5445d

File tree

5 files changed

+179
-21
lines changed

5 files changed

+179
-21
lines changed

rebase-interactive.c

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "strbuf.h"
66
#include "commit-slab.h"
77
#include "config.h"
8+
#include "dir.h"
89

910
static const char edit_todo_list_advice[] =
1011
N_("You can fix this with 'git rebase --edit-todo' "
@@ -97,21 +98,24 @@ int edit_todo_list(struct repository *r, struct todo_list *todo_list,
9798
struct todo_list *new_todo, const char *shortrevisions,
9899
const char *shortonto, unsigned flags)
99100
{
100-
const char *todo_file = rebase_path_todo();
101+
const char *todo_file = rebase_path_todo(),
102+
*todo_backup = rebase_path_todo_backup();
101103
unsigned initial = shortrevisions && shortonto;
104+
int incorrect = 0;
102105

103106
/* If the user is editing the todo list, we first try to parse
104107
* it. If there is an error, we do not return, because the user
105108
* might want to fix it in the first place. */
106109
if (!initial)
107-
todo_list_parse_insn_buffer(r, todo_list->buf.buf, todo_list);
110+
incorrect = todo_list_parse_insn_buffer(r, todo_list->buf.buf, todo_list) |
111+
file_exists(rebase_path_dropped());
108112

109113
if (todo_list_write_to_file(r, todo_list, todo_file, shortrevisions, shortonto,
110114
-1, flags | TODO_LIST_SHORTEN_IDS | TODO_LIST_APPEND_TODO_HELP))
111115
return error_errno(_("could not write '%s'"), todo_file);
112116

113-
if (initial &&
114-
todo_list_write_to_file(r, todo_list, rebase_path_todo_backup(),
117+
if (!incorrect &&
118+
todo_list_write_to_file(r, todo_list, todo_backup,
115119
shortrevisions, shortonto, -1,
116120
(flags | TODO_LIST_APPEND_TODO_HELP) & ~TODO_LIST_SHORTEN_IDS) < 0)
117121
return error(_("could not write '%s'."), rebase_path_todo_backup());
@@ -123,10 +127,23 @@ int edit_todo_list(struct repository *r, struct todo_list *todo_list,
123127
if (initial && new_todo->buf.len == 0)
124128
return -3;
125129

126-
/* For the initial edit, the todo list gets parsed in
127-
* complete_action(). */
128-
if (!initial)
129-
return todo_list_parse_insn_buffer(r, new_todo->buf.buf, new_todo);
130+
if (todo_list_parse_insn_buffer(r, new_todo->buf.buf, new_todo)) {
131+
fprintf(stderr, _(edit_todo_list_advice));
132+
return -4;
133+
}
134+
135+
if (incorrect) {
136+
if (todo_list_check_against_backup(r, new_todo)) {
137+
write_file(rebase_path_dropped(), "");
138+
return -4;
139+
}
140+
141+
if (incorrect > 0)
142+
unlink(rebase_path_dropped());
143+
} else if (todo_list_check(todo_list, new_todo)) {
144+
write_file(rebase_path_dropped(), "");
145+
return -4;
146+
}
130147

131148
return 0;
132149
}
@@ -191,11 +208,27 @@ int todo_list_check(struct todo_list *old_todo, struct todo_list *new_todo)
191208
"the level of warnings.\n"
192209
"The possible behaviours are: ignore, warn, error.\n\n"));
193210

211+
fprintf(stderr, _(edit_todo_list_advice));
212+
194213
leave_check:
195214
clear_commit_seen(&commit_seen);
196215
return res;
197216
}
198217

218+
int todo_list_check_against_backup(struct repository *r, struct todo_list *todo_list)
219+
{
220+
struct todo_list backup = TODO_LIST_INIT;
221+
int res = 0;
222+
223+
if (strbuf_read_file(&backup.buf, rebase_path_todo_backup(), 0) > 0) {
224+
todo_list_parse_insn_buffer(r, backup.buf.buf, &backup);
225+
res = todo_list_check(&backup, todo_list);
226+
}
227+
228+
todo_list_release(&backup);
229+
return res;
230+
}
231+
199232
int check_todo_list_from_file(struct repository *r)
200233
{
201234
struct todo_list old_todo = TODO_LIST_INIT, new_todo = TODO_LIST_INIT;
@@ -214,10 +247,10 @@ int check_todo_list_from_file(struct repository *r)
214247
res = todo_list_parse_insn_buffer(r, old_todo.buf.buf, &old_todo);
215248
if (!res)
216249
res = todo_list_parse_insn_buffer(r, new_todo.buf.buf, &new_todo);
217-
if (!res)
218-
res = todo_list_check(&old_todo, &new_todo);
219250
if (res)
220251
fprintf(stderr, _(edit_todo_list_advice));
252+
if (!res)
253+
res = todo_list_check(&old_todo, &new_todo);
221254
out:
222255
todo_list_release(&old_todo);
223256
todo_list_release(&new_todo);

rebase-interactive.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ void append_todo_help(unsigned keep_empty, int command_count,
1111
int edit_todo_list(struct repository *r, struct todo_list *todo_list,
1212
struct todo_list *new_todo, const char *shortrevisions,
1313
const char *shortonto, unsigned flags);
14+
1415
int todo_list_check(struct todo_list *old_todo, struct todo_list *new_todo);
16+
int todo_list_check_against_backup(struct repository *r,
17+
struct todo_list *todo_list);
1518

1619
int check_todo_list_from_file(struct repository *r);
1720

sequencer.c

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ static GIT_PATH_FUNC(rebase_path, "rebase-merge")
5757
GIT_PATH_FUNC(rebase_path_todo, "rebase-merge/git-rebase-todo")
5858
GIT_PATH_FUNC(rebase_path_todo_backup, "rebase-merge/git-rebase-todo.backup")
5959

60+
GIT_PATH_FUNC(rebase_path_dropped, "rebase-merge/dropped")
61+
6062
/*
6163
* The rebase command lines that have already been processed. A line
6264
* is moved here when it is first handled, before any associated user
@@ -4239,6 +4241,14 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)
42394241
if (is_rebase_i(opts)) {
42404242
if ((res = read_populate_todo(r, &todo_list, opts)))
42414243
goto release_todo_list;
4244+
4245+
if (file_exists(rebase_path_dropped())) {
4246+
if ((res = todo_list_check_against_backup(r, &todo_list)))
4247+
goto release_todo_list;
4248+
4249+
unlink(rebase_path_dropped());
4250+
}
4251+
42424252
if (commit_staged_changes(r, opts, &todo_list)) {
42434253
res = -1;
42444254
goto release_todo_list;
@@ -4985,12 +4995,6 @@ int todo_list_write_to_file(struct repository *r, struct todo_list *todo_list,
49854995
return res;
49864996
}
49874997

4988-
static const char edit_todo_list_advice[] =
4989-
N_("You can fix this with 'git rebase --edit-todo' "
4990-
"and then run 'git rebase --continue'.\n"
4991-
"Or you can abort the rebase with 'git rebase"
4992-
" --abort'.\n");
4993-
49944998
/* skip picking commits whose parents are unchanged */
49954999
static int skip_unnecessary_picks(struct repository *r,
49965000
struct todo_list *todo_list,
@@ -5088,11 +5092,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
50885092
todo_list_release(&new_todo);
50895093

50905094
return error(_("nothing to do"));
5091-
}
5092-
5093-
if (todo_list_parse_insn_buffer(r, new_todo.buf.buf, &new_todo) ||
5094-
todo_list_check(todo_list, &new_todo)) {
5095-
fprintf(stderr, _(edit_todo_list_advice));
5095+
} else if (res == -4) {
50965096
checkout_onto(r, opts, onto_name, &onto->object.oid, orig_head);
50975097
todo_list_release(&new_todo);
50985098

sequencer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const char *git_path_commit_editmsg(void);
1111
const char *git_path_seq_dir(void);
1212
const char *rebase_path_todo(void);
1313
const char *rebase_path_todo_backup(void);
14+
const char *rebase_path_dropped(void);
1415

1516
#define APPEND_SIGNOFF_DEDUP (1u << 0)
1617

t/t3404-rebase-interactive.sh

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,127 @@ test_expect_success 'rebase -i respects rebase.missingCommitsCheck = error' '
14631463
test B = $(git cat-file commit HEAD^ | sed -ne \$p)
14641464
'
14651465

1466+
test_expect_success 'rebase --edit-todo respects rebase.missingCommitsCheck = ignore' '
1467+
test_config rebase.missingCommitsCheck ignore &&
1468+
rebase_setup_and_clean missing-commit &&
1469+
(
1470+
set_fake_editor &&
1471+
FAKE_LINES="break 1 2 3 4 5" git rebase -i --root &&
1472+
FAKE_LINES="1 2 3 4" git rebase --edit-todo &&
1473+
git rebase --continue 2>actual
1474+
) &&
1475+
test D = $(git cat-file commit HEAD | sed -ne \$p) &&
1476+
test_i18ngrep \
1477+
"Successfully rebased and updated refs/heads/missing-commit" \
1478+
actual
1479+
'
1480+
1481+
test_expect_success 'rebase --edit-todo respects rebase.missingCommitsCheck = warn' '
1482+
cat >expect <<-EOF &&
1483+
error: invalid line 1: badcmd $(git rev-list --pretty=oneline --abbrev-commit -1 master~4)
1484+
Warning: some commits may have been dropped accidentally.
1485+
Dropped commits (newer to older):
1486+
- $(git rev-list --pretty=oneline --abbrev-commit -1 master)
1487+
- $(git rev-list --pretty=oneline --abbrev-commit -1 master~4)
1488+
To avoid this message, use "drop" to explicitly remove a commit.
1489+
EOF
1490+
head -n4 expect >expect.2 &&
1491+
tail -n1 expect >>expect.2 &&
1492+
tail -n4 expect.2 >expect.3 &&
1493+
test_config rebase.missingCommitsCheck warn &&
1494+
rebase_setup_and_clean missing-commit &&
1495+
(
1496+
set_fake_editor &&
1497+
test_must_fail env FAKE_LINES="bad 1 2 3 4 5" \
1498+
git rebase -i --root &&
1499+
cp .git/rebase-merge/git-rebase-todo.backup orig &&
1500+
FAKE_LINES="2 3 4" git rebase --edit-todo 2>actual.2 &&
1501+
head -n6 actual.2 >actual &&
1502+
test_i18ncmp expect actual &&
1503+
cp orig .git/rebase-merge/git-rebase-todo &&
1504+
FAKE_LINES="1 2 3 4" git rebase --edit-todo 2>actual.2 &&
1505+
head -n4 actual.2 >actual &&
1506+
test_i18ncmp expect.3 actual &&
1507+
git rebase --continue 2>actual
1508+
) &&
1509+
test D = $(git cat-file commit HEAD | sed -ne \$p) &&
1510+
test_i18ngrep \
1511+
"Successfully rebased and updated refs/heads/missing-commit" \
1512+
actual
1513+
'
1514+
1515+
test_expect_success 'rebase --edit-todo respects rebase.missingCommitsCheck = error' '
1516+
cat >expect <<-EOF &&
1517+
error: invalid line 1: badcmd $(git rev-list --pretty=oneline --abbrev-commit -1 master~4)
1518+
Warning: some commits may have been dropped accidentally.
1519+
Dropped commits (newer to older):
1520+
- $(git rev-list --pretty=oneline --abbrev-commit -1 master)
1521+
- $(git rev-list --pretty=oneline --abbrev-commit -1 master~4)
1522+
To avoid this message, use "drop" to explicitly remove a commit.
1523+
1524+
Use '\''git config rebase.missingCommitsCheck'\'' to change the level of warnings.
1525+
The possible behaviours are: ignore, warn, error.
1526+
1527+
You can fix this with '\''git rebase --edit-todo'\'' and then run '\''git rebase --continue'\''.
1528+
Or you can abort the rebase with '\''git rebase --abort'\''.
1529+
EOF
1530+
tail -n11 expect >expect.2 &&
1531+
head -n3 expect.2 >expect.3 &&
1532+
tail -n7 expect.2 >>expect.3 &&
1533+
test_config rebase.missingCommitsCheck error &&
1534+
rebase_setup_and_clean missing-commit &&
1535+
(
1536+
set_fake_editor &&
1537+
test_must_fail env FAKE_LINES="bad 1 2 3 4 5" \
1538+
git rebase -i --root &&
1539+
cp .git/rebase-merge/git-rebase-todo.backup orig &&
1540+
test_must_fail env FAKE_LINES="2 3 4" \
1541+
git rebase --edit-todo 2>actual &&
1542+
test_i18ncmp expect actual &&
1543+
test_must_fail git rebase --continue 2>actual &&
1544+
test_i18ncmp expect.2 actual &&
1545+
test_must_fail git rebase --edit-todo &&
1546+
cp orig .git/rebase-merge/git-rebase-todo &&
1547+
test_must_fail env FAKE_LINES="1 2 3 4" \
1548+
git rebase --edit-todo 2>actual &&
1549+
test_i18ncmp expect.3 actual &&
1550+
test_must_fail git rebase --continue 2>actual &&
1551+
test_i18ncmp expect.3 actual &&
1552+
cp orig .git/rebase-merge/git-rebase-todo &&
1553+
FAKE_LINES="1 2 3 4 drop 5" git rebase --edit-todo &&
1554+
git rebase --continue 2>actual
1555+
) &&
1556+
test D = $(git cat-file commit HEAD | sed -ne \$p) &&
1557+
test_i18ngrep \
1558+
"Successfully rebased and updated refs/heads/missing-commit" \
1559+
actual
1560+
'
1561+
1562+
test_expect_success 'rebase.missingCommitsCheck = error after resolving conflicts' '
1563+
test_config rebase.missingCommitsCheck error &&
1564+
(
1565+
set_fake_editor &&
1566+
FAKE_LINES="drop 1 break 2 3 4" git rebase -i A E
1567+
) &&
1568+
git rebase --edit-todo &&
1569+
test_must_fail git rebase --continue &&
1570+
echo x >file1 &&
1571+
git add file1 &&
1572+
git rebase --continue
1573+
'
1574+
1575+
test_expect_success 'rebase.missingCommitsCheck = error when editing for a second time' '
1576+
test_config rebase.missingCommitsCheck error &&
1577+
(
1578+
set_fake_editor &&
1579+
FAKE_LINES="1 break 2 3" git rebase -i A D &&
1580+
cp .git/rebase-merge/git-rebase-todo todo &&
1581+
test_must_fail env FAKE_LINES=2 git rebase --edit-todo &&
1582+
GIT_SEQUENCE_EDITOR="cp todo" git rebase --edit-todo &&
1583+
git rebase --continue
1584+
)
1585+
'
1586+
14661587
test_expect_success 'respects rebase.abbreviateCommands with fixup, squash and exec' '
14671588
rebase_setup_and_clean abbrevcmd &&
14681589
test_commit "first" file1.txt "first line" first &&

0 commit comments

Comments
 (0)