Merge branch 'pw/status-rebase-todo' into seen

The display of the rebase todo list in "git status" has been
improved to correctly abbreviate object IDs for more commands and
avoid misinterpreting refs as object IDs.

* pw/status-rebase-todo:
  status: improve rebase todo list parsing
  sequencer: factor out parsing of todo commands
This commit is contained in:
Junio C Hamano
2026-06-04 08:14:06 +09:00
4 changed files with 186 additions and 62 deletions

View File

@@ -2627,6 +2627,27 @@ static int is_command(enum todo_command command, const char **bol)
return 0;
}
bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd)
{
const char *s = *p;
for (int i = 0; i < TODO_COMMENT; i++)
if (is_command(i, p)) {
*cmd = i;
return true;
}
if (starts_with(s, comment_line_str)) {
*cmd = TODO_COMMENT;
return true;
} else if (s[0] == '\n' || (s[0] == '\r' && s[1] == '\n') || !s[0]) {
*cmd = TODO_COMMENT;
return true;
}
return false;
}
static int check_label_or_ref_arg(enum todo_command command, const char *arg)
{
switch (command) {
@@ -2716,30 +2737,24 @@ static int parse_insn_line(struct repository *r, struct replay_opts *opts,
{
struct object_id commit_oid;
char *end_of_object_name;
int i, saved, status, padding;
int saved, status, padding;
item->flags = 0;
/* left-trim */
bol += strspn(bol, " \t");
if (bol == eol || *bol == '\r' || starts_with_mem(bol, eol - bol, comment_line_str)) {
item->command = TODO_COMMENT;
if (!sequencer_parse_todo_command(&bol, &item->command))
return error(_("invalid command '%.*s'"),
(int)strcspn(bol, " \t\r\n"), bol);
if (item->command == TODO_COMMENT) {
item->commit = NULL;
item->arg_offset = bol - buf;
item->arg_len = eol - bol;
return 0;
}
for (i = 0; i < TODO_COMMENT; i++)
if (is_command(i, &bol)) {
item->command = i;
break;
}
if (i >= TODO_COMMENT)
return error(_("invalid command '%.*s'"),
(int)strcspn(bol, " \t\r\n"), bol);
/* Eat up extra spaces/ tabs before object name */
padding = strspn(bol, " \t");
bol += padding;

View File

@@ -265,6 +265,14 @@ int read_author_script(const char *path, char **name, char **email, char **date,
int write_basic_state(struct replay_opts *opts, const char *head_name,
struct commit *onto, const struct object_id *orig_head);
void sequencer_post_commit_cleanup(struct repository *r, int verbose);
/*
* Try to parse the todo command pointed to by *p. On success sets cmd,
* advances p and returns true. On failure returns false, leaves p and
* cmd unchanged.
*/
bool sequencer_parse_todo_command(const char **p, enum todo_command *cmd);
int sequencer_get_last_command(struct repository* r,
enum replay_action *action);
int sequencer_determine_whence(struct repository *r, enum commit_whence *whence);

View File

@@ -224,7 +224,7 @@ test_expect_success 'status when splitting a commit' '
COMMIT3=$(git rev-parse --short split_commit) &&
test_commit four_split main.txt four &&
COMMIT4=$(git rev-parse --short split_commit) &&
FAKE_LINES="1 edit 2 3" &&
FAKE_LINES="reword 1 edit 2 fixup_-C 3" &&
export FAKE_LINES &&
test_when_finished "git rebase --abort" &&
ONTO=$(git rev-parse --short HEAD~3) &&
@@ -233,10 +233,10 @@ test_expect_success 'status when splitting a commit' '
cat >expected <<EOF &&
interactive rebase in progress; onto $ONTO
Last commands done (2 commands done):
pick $COMMIT2 # two_split
reword $COMMIT2 # two_split
edit $COMMIT3 # three_split
Next command to do (1 remaining command):
pick $COMMIT4 # four_split
fixup -C $COMMIT4 # four_split
(use "git rebase --edit-todo" to view and edit)
You are currently splitting a commit while rebasing branch '\''split_commit'\'' on '\''$ONTO'\''.
(Once your working directory is clean, run "git rebase --continue")
@@ -297,7 +297,7 @@ test_expect_success 'prepare for several edits' '
test_expect_success 'status: (continue first edit) second edit' '
FAKE_LINES="edit 1 edit 2 3" &&
FAKE_LINES="edit 1 edit 2 drop 3" &&
export FAKE_LINES &&
test_when_finished "git rebase --abort" &&
COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -312,7 +312,7 @@ Last commands done (2 commands done):
edit $COMMIT2 # two_edits
edit $COMMIT3 # three_edits
Next command to do (1 remaining command):
pick $COMMIT4 # four_edits
drop $COMMIT4 # four_edits
(use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
(use "git commit --amend" to amend the current commit)
@@ -327,7 +327,7 @@ EOF
test_expect_success 'status: (continue first edit) second edit and split' '
git reset --hard several_edits &&
FAKE_LINES="edit 1 edit 2 3" &&
FAKE_LINES="edit 1 edit 2 squash 3" &&
export FAKE_LINES &&
test_when_finished "git rebase --abort" &&
COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -343,7 +343,7 @@ Last commands done (2 commands done):
edit $COMMIT2 # two_edits
edit $COMMIT3 # three_edits
Next command to do (1 remaining command):
pick $COMMIT4 # four_edits
squash $COMMIT4 # four_edits
(use "git rebase --edit-todo" to view and edit)
You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
(Once your working directory is clean, run "git rebase --continue")
@@ -362,7 +362,7 @@ EOF
test_expect_success 'status: (continue first edit) second edit and amend' '
git reset --hard several_edits &&
FAKE_LINES="edit 1 edit 2 3" &&
FAKE_LINES="edit 1 edit 2 fixup 3" &&
export FAKE_LINES &&
test_when_finished "git rebase --abort" &&
COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -378,7 +378,7 @@ Last commands done (2 commands done):
edit $COMMIT2 # two_edits
edit $COMMIT3 # three_edits
Next command to do (1 remaining command):
pick $COMMIT4 # four_edits
fixup $COMMIT4 # four_edits
(use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
(use "git commit --amend" to amend the current commit)
@@ -393,7 +393,7 @@ EOF
test_expect_success 'status: (amend first edit) second edit' '
git reset --hard several_edits &&
FAKE_LINES="edit 1 edit 2 3" &&
FAKE_LINES="edit 1 edit 2 fixup_-c 3" &&
export FAKE_LINES &&
test_when_finished "git rebase --abort" &&
COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -409,7 +409,7 @@ Last commands done (2 commands done):
edit $COMMIT2 # two_edits
edit $COMMIT3 # three_edits
Next command to do (1 remaining command):
pick $COMMIT4 # four_edits
fixup -c $COMMIT4 # four_edits
(use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
(use "git commit --amend" to amend the current commit)
@@ -460,14 +460,20 @@ EOF
test_expect_success 'status: (amend first edit) second edit and amend' '
git reset --hard several_edits &&
FAKE_LINES="edit 1 edit 2 3" &&
export FAKE_LINES &&
test_when_finished "git rebase --abort" &&
COMMIT2=$(git rev-parse --short several_edits^^) &&
COMMIT3=$(git rev-parse --short several_edits^) &&
COMMIT4=$(git rev-parse --short several_edits) &&
ONTO=$(git rev-parse --short HEAD~3) &&
git rebase -i HEAD~3 &&
cat >todo <<-EOF &&
edit several_edits^^ # two_edits
edit several_edits^ # three_edits
merge $(git rev-parse main) $(git rev-parse several_edits)
EOF
(
set_replace_editor todo &&
git rebase -i HEAD~3
) &&
git commit --amend -m "c" &&
git rebase --continue &&
git commit --amend -m "d" &&
@@ -477,7 +483,7 @@ Last commands done (2 commands done):
edit $COMMIT2 # two_edits
edit $COMMIT3 # three_edits
Next command to do (1 remaining command):
pick $COMMIT4 # four_edits
merge $(git rev-parse --short main) $COMMIT4
(use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
(use "git commit --amend" to amend the current commit)
@@ -525,14 +531,21 @@ EOF
test_expect_success 'status: (split first edit) second edit and split' '
git reset --hard several_edits &&
FAKE_LINES="edit 1 edit 2 3" &&
export FAKE_LINES &&
test_when_finished "git rebase --abort" &&
COMMIT2=$(git rev-parse --short several_edits^^) &&
COMMIT3=$(git rev-parse --short several_edits^) &&
COMMIT4=$(git rev-parse --short several_edits) &&
cat >todo <<-EOF &&
edit several_edits^^ # two_edits
edit several_edits^ # three_edits
reset $(git rev-parse main)
merge -C several_edits topic # title
EOF
ONTO=$(git rev-parse --short HEAD~3) &&
git rebase -i HEAD~3 &&
(
set_replace_editor todo &&
git rebase -i HEAD~3
) &&
git reset HEAD^ &&
git add main.txt &&
git commit --amend -m "f" &&
@@ -543,8 +556,9 @@ interactive rebase in progress; onto $ONTO
Last commands done (2 commands done):
edit $COMMIT2 # two_edits
edit $COMMIT3 # three_edits
Next command to do (1 remaining command):
pick $COMMIT4 # four_edits
Next commands to do (2 remaining commands):
reset $(git rev-parse --short main)
merge -C $COMMIT4 topic # title
(use "git rebase --edit-todo" to view and edit)
You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
(Once your working directory is clean, run "git rebase --continue")
@@ -563,14 +577,21 @@ EOF
test_expect_success 'status: (split first edit) second edit and amend' '
git reset --hard several_edits &&
FAKE_LINES="edit 1 edit 2 3" &&
export FAKE_LINES &&
test_when_finished "git rebase --abort" &&
git branch cafe main &&
COMMIT2=$(git rev-parse --short several_edits^^) &&
COMMIT3=$(git rev-parse --short several_edits^) &&
COMMIT4=$(git rev-parse --short several_edits) &&
cat >todo <<-EOF &&
edit several_edits^^ # two_edits
edit several_edits^ # three_edits
update-ref refs/heads/main
reset cafe
EOF
ONTO=$(git rev-parse --short HEAD~3) &&
git rebase -i HEAD~3 &&
(
set_replace_editor todo &&
git rebase -i HEAD~3
) &&
git reset HEAD^ &&
git add main.txt &&
git commit --amend -m "g" &&
@@ -581,8 +602,9 @@ interactive rebase in progress; onto $ONTO
Last commands done (2 commands done):
edit $COMMIT2 # two_edits
edit $COMMIT3 # three_edits
Next command to do (1 remaining command):
pick $COMMIT4 # four_edits
Next commands to do (2 remaining commands):
update-ref refs/heads/main
reset cafe
(use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
(use "git commit --amend" to amend the current commit)

View File

@@ -1365,35 +1365,118 @@ static int split_commit_in_progress(struct wt_status *s)
return split_in_progress;
}
/*
* If the whitespace-delimited token starting at or just after *pp *
* is a hex object id that is longer than its default abbreviation, *
* abbreviate it in-place, shrinking `line` accordingly. On return
* *pp points one past the (possibly abbreviated) token. Leaves both
* `line` and *pp-advanced-past-the-token unchanged in all other cases
* (non-hex token, unresolvable, or a refname that happens to consist
* only of hex digits).
*/
static void abbrev_oid_in_line(struct repository *r,
struct strbuf *line, char **pp)
{
char *p = *pp;
char *end_of_object_name, saved;
const char *abbrev;
struct object_id oid;
bool have_oid;
p += strspn(p, " \t");
end_of_object_name = p + strcspn(p, " \t");
/*
* For "merge" and "reset" the object name may be a label or
* ref rather than a hex object id. Only abbreviate the object
* name if it is a hex object id.
*/
for (const char *q = p; q < end_of_object_name; q++) {
if (!isxdigit(*q))
goto out;
}
saved = *end_of_object_name;
*end_of_object_name = '\0';
have_oid = !repo_get_oid(r, p, &oid);
*end_of_object_name = saved;
if (!have_oid)
goto out; /* object name was a label */
abbrev = repo_find_unique_abbrev(r, &oid, DEFAULT_ABBREV);
if (!starts_with(p, abbrev))
goto out; /* object name was a refname containing only xdigits */
p += strlen(abbrev);
strbuf_remove(line, p - line->buf, end_of_object_name - p);
end_of_object_name = p;
out:
*pp = end_of_object_name;
}
static void skip_dash_c(char **pp)
{
char *p = *pp;
p += strspn(p, " \t");
/* The (void) cast is required to silence -Wunused-value */
(void)(skip_prefix(p, "-C", &p) || skip_prefix(p, "-c", &p));
*pp = p;
}
/*
* Turn
* "pick d6a2f0303e897ec257dd0e0a39a5ccb709bc2047 some message"
* into
* "pick d6a2f03 some message"
*
* The function assumes that the line does not contain useless spaces
* before or after the command.
* Returns false on comment lines, true otherwise
*/
static void abbrev_oid_in_line(struct repository *r, struct strbuf *line)
static bool format_todo_line(struct repository *r, struct strbuf *line)
{
struct string_list split = STRING_LIST_INIT_DUP;
struct object_id oid;
enum todo_command cmd;
char *p = line->buf;
if (starts_with(line->buf, "exec ") ||
starts_with(line->buf, "x ") ||
starts_with(line->buf, "label ") ||
starts_with(line->buf, "l "))
return;
if (!sequencer_parse_todo_command((const char**)&p, &cmd))
return true; /* keep invalid lines */
if ((2 <= string_list_split(&split, line->buf, " ", 2)) &&
!repo_get_oid(r, split.items[1].string, &oid)) {
strbuf_reset(line);
strbuf_addf(line, "%s ", split.items[0].string);
strbuf_add_unique_abbrev(line, &oid, DEFAULT_ABBREV);
for (size_t i = 2; i < split.nr; i++)
strbuf_addf(line, " %s", split.items[i].string);
switch (cmd) {
case TODO_COMMENT:
return false;
case TODO_MERGE:
skip_dash_c(&p);
while (true) {
p += strspn(p, " \t");
if (!p[0] || (p[0] == '#' && (!p[1] || isspace(p[1]))))
break;
abbrev_oid_in_line(r, line, &p);
}
break;
case TODO_FIXUP:
skip_dash_c(&p);
/* fallthrough */
case TODO_DROP:
case TODO_EDIT:
case TODO_PICK:
case TODO_RESET:
case TODO_REVERT:
case TODO_REWORD:
case TODO_SQUASH:
abbrev_oid_in_line(r, line, &p);
break;
/*
* Avoid "default" and instead list all the other commands so
* that -Wswitch (which is included in -Wall) warns if a new
* command is added without handling it in this function.
*/
case TODO_BREAK:
case TODO_EXEC:
case TODO_LABEL:
case TODO_NOOP:
case TODO_UPDATE_REF:
break;
}
string_list_clear(&split, 0);
return true;
}
static int read_rebase_todolist(struct repository *r, const char *fname, struct string_list *lines)
@@ -1411,13 +1494,9 @@ static int read_rebase_todolist(struct repository *r, const char *fname, struct
repo_git_path_replace(r, &buf, "%s", fname));
}
while (!strbuf_getline_lf(&buf, f)) {
if (starts_with(buf.buf, comment_line_str))
continue;
strbuf_trim(&buf);
if (!buf.len)
continue;
abbrev_oid_in_line(r, &buf);
string_list_append(lines, buf.buf);
if (format_todo_line(r, &buf))
string_list_append(lines, buf.buf);
}
fclose(f);