status: improve rebase todo list parsing

When there is rebase in progress "git status" displays the last couple of completed and the next couple of pending commands from the todo list. When it does this it tries to abbreviate the object ids of the commits to be picked. Unfortunately it does not abbreviate the object ids when the line starts with "fixup -C" or "merge -C". It also mistakenly replaces the refname in "reset main" and "update-ref refs/heads/main" with the object id that the ref points to. Fix this by using the function added in the last commit to parse the command name and only try to abbreviate the argument for commands that take an object id. If a command accepts a label then try to resolve the object name as a label first and only if that fails try to resolve it as an object_id. When trying to abbreviate an object id, only replace the object name if it starts with the abbreviated object id so that tag or branch names that contain only hex digits are left unchanged. Comments are now processed after stripping any leading whitespace from the line. This matches what the sequencer does in parse_insn_line(). The existing test cases are updated to test a wider variety of commands. Only the pending commands in the tests are changed to avoid removing existing coverage. Helped-by: Elijah Newren <newren@gmail.com> Signed-off-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Phillip Wood committed Jun 23, 2026 at 16:53 UTC 6f34e5f9e3b664fcd65a2a0695dff16b0ee04b35
2 files changed +172 -50
t/t7512-status-help.sh
+48 -26
@@ -224,7 +224,7 @@ test_expect_success 'status when splitting a commit' '
224 COMMIT3=$(git rev-parse --short split_commit) &&
225 test_commit four_split main.txt four &&
226 COMMIT4=$(git rev-parse --short split_commit) &&
227 - FAKE_LINES="1 edit 2 3" &&
227 + FAKE_LINES="reword 1 edit 2 fixup_-C 3" &&
228 export FAKE_LINES &&
229 test_when_finished "git rebase --abort" &&
230 ONTO=$(git rev-parse --short HEAD~3) &&
@@ -233,10 +233,10 @@ test_expect_success 'status when splitting a commit' '
233 cat >expected <<EOF &&
234 interactive rebase in progress; onto $ONTO
235 Last commands done (2 commands done):
236 - pick $COMMIT2 # two_split
236 + reword $COMMIT2 # two_split
237 edit $COMMIT3 # three_split
238 Next command to do (1 remaining command):
239 - pick $COMMIT4 # four_split
239 + fixup -C $COMMIT4 # four_split
240 (use "git rebase --edit-todo" to view and edit)
241 You are currently splitting a commit while rebasing branch '\''split_commit'\'' on '\''$ONTO'\''.
242 (Once your working directory is clean, run "git rebase --continue")
@@ -297,7 +297,7 @@ test_expect_success 'prepare for several edits' '
297
298
299 test_expect_success 'status: (continue first edit) second edit' '
300 - FAKE_LINES="edit 1 edit 2 3" &&
300 + FAKE_LINES="edit 1 edit 2 drop 3" &&
301 export FAKE_LINES &&
302 test_when_finished "git rebase --abort" &&
303 COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -312,7 +312,7 @@ Last commands done (2 commands done):
312 edit $COMMIT2 # two_edits
313 edit $COMMIT3 # three_edits
314 Next command to do (1 remaining command):
315 - pick $COMMIT4 # four_edits
315 + drop $COMMIT4 # four_edits
316 (use "git rebase --edit-todo" to view and edit)
317 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
318 (use "git commit --amend" to amend the current commit)
@@ -327,7 +327,7 @@ EOF
327
328 test_expect_success 'status: (continue first edit) second edit and split' '
329 git reset --hard several_edits &&
330 - FAKE_LINES="edit 1 edit 2 3" &&
330 + FAKE_LINES="edit 1 edit 2 squash 3" &&
331 export FAKE_LINES &&
332 test_when_finished "git rebase --abort" &&
333 COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -343,7 +343,7 @@ Last commands done (2 commands done):
343 edit $COMMIT2 # two_edits
344 edit $COMMIT3 # three_edits
345 Next command to do (1 remaining command):
346 - pick $COMMIT4 # four_edits
346 + squash $COMMIT4 # four_edits
347 (use "git rebase --edit-todo" to view and edit)
348 You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
349 (Once your working directory is clean, run "git rebase --continue")
@@ -362,7 +362,7 @@ EOF
362
363 test_expect_success 'status: (continue first edit) second edit and amend' '
364 git reset --hard several_edits &&
365 - FAKE_LINES="edit 1 edit 2 3" &&
365 + FAKE_LINES="edit 1 edit 2 fixup 3" &&
366 export FAKE_LINES &&
367 test_when_finished "git rebase --abort" &&
368 COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -378,7 +378,7 @@ Last commands done (2 commands done):
378 edit $COMMIT2 # two_edits
379 edit $COMMIT3 # three_edits
380 Next command to do (1 remaining command):
381 - pick $COMMIT4 # four_edits
381 + fixup $COMMIT4 # four_edits
382 (use "git rebase --edit-todo" to view and edit)
383 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
384 (use "git commit --amend" to amend the current commit)
@@ -393,7 +393,7 @@ EOF
393
394 test_expect_success 'status: (amend first edit) second edit' '
395 git reset --hard several_edits &&
396 - FAKE_LINES="edit 1 edit 2 3" &&
396 + FAKE_LINES="edit 1 edit 2 fixup_-c 3" &&
397 export FAKE_LINES &&
398 test_when_finished "git rebase --abort" &&
399 COMMIT2=$(git rev-parse --short several_edits^^) &&
@@ -409,7 +409,7 @@ Last commands done (2 commands done):
409 edit $COMMIT2 # two_edits
410 edit $COMMIT3 # three_edits
411 Next command to do (1 remaining command):
412 - pick $COMMIT4 # four_edits
412 + fixup -c $COMMIT4 # four_edits
413 (use "git rebase --edit-todo" to view and edit)
414 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
415 (use "git commit --amend" to amend the current commit)
@@ -460,14 +460,20 @@ EOF
460
461 test_expect_success 'status: (amend first edit) second edit and amend' '
462 git reset --hard several_edits &&
463 - FAKE_LINES="edit 1 edit 2 3" &&
464 - export FAKE_LINES &&
463 test_when_finished "git rebase --abort" &&
464 COMMIT2=$(git rev-parse --short several_edits^^) &&
465 COMMIT3=$(git rev-parse --short several_edits^) &&
466 COMMIT4=$(git rev-parse --short several_edits) &&
467 ONTO=$(git rev-parse --short HEAD~3) &&
470 - git rebase -i HEAD~3 &&
468 + cat >todo <<-EOF &&
469 + edit several_edits^^ # two_edits
470 + edit several_edits^ # three_edits
471 + merge $(git rev-parse main) $(git rev-parse several_edits)
472 + EOF
473 + (
474 + set_replace_editor todo &&
475 + git rebase -i HEAD~3
476 + ) &&
477 git commit --amend -m "c" &&
478 git rebase --continue &&
479 git commit --amend -m "d" &&
@@ -477,7 +483,7 @@ Last commands done (2 commands done):
483 edit $COMMIT2 # two_edits
484 edit $COMMIT3 # three_edits
485 Next command to do (1 remaining command):
480 - pick $COMMIT4 # four_edits
486 + merge $(git rev-parse --short main) $COMMIT4
487 (use "git rebase --edit-todo" to view and edit)
488 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
489 (use "git commit --amend" to amend the current commit)
@@ -525,14 +531,21 @@ EOF
531
532 test_expect_success 'status: (split first edit) second edit and split' '
533 git reset --hard several_edits &&
528 - FAKE_LINES="edit 1 edit 2 3" &&
529 - export FAKE_LINES &&
534 test_when_finished "git rebase --abort" &&
535 COMMIT2=$(git rev-parse --short several_edits^^) &&
536 COMMIT3=$(git rev-parse --short several_edits^) &&
537 COMMIT4=$(git rev-parse --short several_edits) &&
538 + cat >todo <<-EOF &&
539 + edit several_edits^^ # two_edits
540 + edit several_edits^ # three_edits
541 + reset $(git rev-parse main)
542 + merge -C several_edits topic # title
543 + EOF
544 ONTO=$(git rev-parse --short HEAD~3) &&
535 - git rebase -i HEAD~3 &&
545 + (
546 + set_replace_editor todo &&
547 + git rebase -i HEAD~3
548 + ) &&
549 git reset HEAD^ &&
550 git add main.txt &&
551 git commit --amend -m "f" &&
@@ -543,8 +556,9 @@ interactive rebase in progress; onto $ONTO
556 Last commands done (2 commands done):
557 edit $COMMIT2 # two_edits
558 edit $COMMIT3 # three_edits
546 -Next command to do (1 remaining command):
547 - pick $COMMIT4 # four_edits
559 +Next commands to do (2 remaining commands):
560 + reset $(git rev-parse --short main)
561 + merge -C $COMMIT4 topic # title
562 (use "git rebase --edit-todo" to view and edit)
563 You are currently splitting a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
564 (Once your working directory is clean, run "git rebase --continue")
@@ -563,14 +577,21 @@ EOF
577
578 test_expect_success 'status: (split first edit) second edit and amend' '
579 git reset --hard several_edits &&
566 - FAKE_LINES="edit 1 edit 2 3" &&
567 - export FAKE_LINES &&
580 test_when_finished "git rebase --abort" &&
581 + git branch cafe main &&
582 COMMIT2=$(git rev-parse --short several_edits^^) &&
583 COMMIT3=$(git rev-parse --short several_edits^) &&
571 - COMMIT4=$(git rev-parse --short several_edits) &&
584 + cat >todo <<-EOF &&
585 + edit several_edits^^ # two_edits
586 + edit several_edits^ # three_edits
587 + update-ref refs/heads/main
588 + reset cafe
589 + EOF
590 ONTO=$(git rev-parse --short HEAD~3) &&
573 - git rebase -i HEAD~3 &&
591 + (
592 + set_replace_editor todo &&
593 + git rebase -i HEAD~3
594 + ) &&
595 git reset HEAD^ &&
596 git add main.txt &&
597 git commit --amend -m "g" &&
@@ -581,8 +602,9 @@ interactive rebase in progress; onto $ONTO
602 Last commands done (2 commands done):
603 edit $COMMIT2 # two_edits
604 edit $COMMIT3 # three_edits
584 -Next command to do (1 remaining command):
585 - pick $COMMIT4 # four_edits
605 +Next commands to do (2 remaining commands):
606 + update-ref refs/heads/main
607 + reset cafe
608 (use "git rebase --edit-todo" to view and edit)
609 You are currently editing a commit while rebasing branch '\''several_edits'\'' on '\''$ONTO'\''.
610 (use "git commit --amend" to amend the current commit)
wt-status.c
+124 -24
@@ -1365,35 +1365,139 @@ static int split_commit_in_progress(struct wt_status *s)
1365 return split_in_progress;
1366 }
1367
1368 +/*
1369 + * If the whitespace-delimited token starting at or just after *pp
1370 + * is a hex object id that is longer than its default abbreviation,
1371 + * abbreviate it in-place, shrinking `line` accordingly. On return
1372 + * *pp points one past the (possibly abbreviated) token. Leaves both
1373 + * `line` and *pp-advanced-past-the-token unchanged in all other cases
1374 + * (non-hex token, label name, unresolvable, or a refname that happens
1375 + * to consist only of hex digits).
1376 + */
1377 +static void abbrev_oid_in_line(struct repository *r, struct strbuf *scratch,
1378 + struct strbuf *line, bool maybe_label, char **pp)
1379 +{
1380 + char *p = *pp;
1381 + char *end_of_object_name, saved;
1382 + const char *abbrev;
1383 + struct object_id oid;
1384 + bool have_oid;
1385 +
1386 + p += strspn(p, " \t");
1387 + end_of_object_name = p + strcspn(p, " \t");
1388 + /*
1389 + * For "merge" and "reset" the object name may be a label or
1390 + * ref rather than a hex object id. Only abbreviate the object
1391 + * name if it is a hex object id.
1392 + */
1393 + for (const char *q = p; q < end_of_object_name; q++) {
1394 + if (!isxdigit(*q))
1395 + goto out;
1396 + }
1397 + if (maybe_label) {
1398 + strbuf_reset(scratch);
1399 + strbuf_addf(scratch, "refs/rewritten/%.*s",
1400 + (int)(end_of_object_name - p), p);
1401 + if (refs_ref_exists(get_main_ref_store(r), scratch->buf))
1402 + goto out; /* object name was a label */
1403 + }
1404 + saved = *end_of_object_name;
1405 + *end_of_object_name = '\0';
1406 + have_oid = !repo_get_oid(r, p, &oid);
1407 + *end_of_object_name = saved;
1408 + if (!have_oid)
1409 + goto out; /* invalid object name */
1410 + abbrev = repo_find_unique_abbrev(r, &oid, DEFAULT_ABBREV);
1411 + if (!starts_with(p, abbrev))
1412 + goto out; /* object name was a refname containing only xdigits */
1413 + p += strlen(abbrev);
1414 + strbuf_remove(line, p - line->buf, end_of_object_name - p);
1415 + end_of_object_name = p;
1416 +out:
1417 + *pp = end_of_object_name;
1418 +}
1419 +
1420 +/* Skip "[ \t]*(-[cC])?", returns true if "-c/-C" was skipped. */
1421 +static bool skip_dash_c(char **pp)
1422 +{
1423 + bool ret;
1424 + char *p = *pp;
1425 +
1426 + p += strspn(p, " \t");
1427 + ret = skip_prefix(p, "-C", &p) || skip_prefix(p, "-c", &p);
1428 + *pp = p;
1429 +
1430 + return ret;
1431 +}
1432 +
1433 /*
1434 * Turn
1435 * "pick d6a2f0303e897ec257dd0e0a39a5ccb709bc2047 some message"
1436 * into
1437 * "pick d6a2f03 some message"
1438 *
1374 - * The function assumes that the line does not contain useless spaces
1375 - * before or after the command.
1439 + * Returns false on comment lines, true otherwise
1440 */
1377 -static void abbrev_oid_in_line(struct repository *r, struct strbuf *line)
1441 +static bool format_todo_line(struct repository *r, struct strbuf *line)
1442 {
1379 - struct string_list split = STRING_LIST_INIT_DUP;
1380 - struct object_id oid;
1443 + enum todo_command cmd;
1444 + struct strbuf scratch = STRBUF_INIT;
1445 + char *p = line->buf;
1446
1382 - if (starts_with(line->buf, "exec ") ||
1383 - starts_with(line->buf, "x ") ||
1384 - starts_with(line->buf, "label ") ||
1385 - starts_with(line->buf, "l "))
1386 - return;
1447 + if (!sequencer_parse_todo_command((const char **)&p, &cmd))
1448 + return true; /* keep invalid lines */
1449 +
1450 + switch (cmd) {
1451 + case TODO_COMMENT:
1452 + return false;
1453 +
1454 + case TODO_MERGE: {
1455 + /*
1456 + * The argument to -C cannot be a label, but the parents
1457 + * can be labels.
1458 + */
1459 + bool maybe_label = !skip_dash_c(&p);
1460 +
1461 + while (true) {
1462 + p += strspn(p, " \t");
1463 + if (!p[0] || (p[0] == '#' && (!p[1] || isspace(p[1]))))
1464 + break;
1465 + abbrev_oid_in_line(r, &scratch, line, maybe_label, &p);
1466 + maybe_label = true;
1467 + }
1468 + break;
1469 + }
1470
1388 - if ((2 <= string_list_split(&split, line->buf, " ", 2)) &&
1389 - !repo_get_oid(r, split.items[1].string, &oid)) {
1390 - strbuf_reset(line);
1391 - strbuf_addf(line, "%s ", split.items[0].string);
1392 - strbuf_add_unique_abbrev(line, &oid, DEFAULT_ABBREV);
1393 - for (size_t i = 2; i < split.nr; i++)
1394 - strbuf_addf(line, " %s", split.items[i].string);
1471 + case TODO_FIXUP:
1472 + skip_dash_c(&p);
1473 + /* fallthrough */
1474 + case TODO_DROP:
1475 + case TODO_EDIT:
1476 + case TODO_PICK:
1477 + case TODO_REVERT:
1478 + case TODO_REWORD:
1479 + case TODO_SQUASH:
1480 + abbrev_oid_in_line(r, &scratch, line, false, &p);
1481 + break;
1482 +
1483 + case TODO_RESET:
1484 + abbrev_oid_in_line(r, &scratch, line, true, &p);
1485 + break;
1486 + /*
1487 + * Avoid "default" and instead list all the other commands so
1488 + * that -Wswitch (which is included in -Wall) warns if a new
1489 + * command is added without handling it in this function.
1490 + */
1491 + case TODO_BREAK:
1492 + case TODO_EXEC:
1493 + case TODO_LABEL:
1494 + case TODO_NOOP:
1495 + case TODO_UPDATE_REF:
1496 + break;
1497 }
1396 - string_list_clear(&split, 0);
1498 +
1499 + strbuf_release(&scratch);
1500 + return true;
1501 }
1502
1503 static int read_rebase_todolist(struct repository *r, const char *fname, struct string_list *lines)
@@ -1411,13 +1515,9 @@ static int read_rebase_todolist(struct repository *r, const char *fname, struct
1515 repo_git_path_replace(r, &buf, "%s", fname));
1516 }
1517 while (!strbuf_getline_lf(&buf, f)) {
1414 - if (starts_with(buf.buf, comment_line_str))
1415 - continue;
1518 strbuf_trim(&buf);
1417 - if (!buf.len)
1418 - continue;
1419 - abbrev_oid_in_line(r, &buf);
1420 - string_list_append(lines, buf.buf);
1519 + if (format_todo_line(r, &buf))
1520 + string_list_append(lines, buf.buf);
1521 }
1522 fclose(f);
1523