diff --git a/docs/src/commands/status.md b/docs/src/commands/status.md index 1379367..877deb5 100644 --- a/docs/src/commands/status.md +++ b/docs/src/commands/status.md @@ -33,12 +33,12 @@ The status displays a branch-aware commit graph using UTF-8 box-drawing characte │ ⁕ untracked.txt │ │╭─ fb [feature-b] ✓ -│● mqt Fix bug in feature B d0472f9 -│● pkz Start feature B 7a067a9 +│● mqt d0472f9 Fix bug in feature B +│● pkz 7a067a9 Start feature B ├╯ │ │╭─ fa [feature-a] ↑ -│● rsv Add feature A 2ee61e1 +│● rsv 2ee61e1 Add feature A ├╯ │ ● ff1b247 (upstream) [origin/main] Initial commit @@ -81,7 +81,7 @@ The graph is rendered top-to-bottom with these sections: Each branch, commit, and file in the output is assigned a short ID — a compact identifier you can use with other *git-loom* commands. What you see in the status is what you type. -A commit's short ID comes first on its line, in a fixed column, with the abbreviated hash at the end of the line. Commits that carry a `Change-Id` trailer (every commit loom creates, see [`loom.changeId`](../configuration.md#loomchangeid)) get a **persistent** ID made of the letters `k`–`z`, such as `mqt`: it is derived from the Change-Id, not from the hash, so it survives `update`, `fold`, `swap`, `split`, and every other rewrite. Any longer prefix of the ID also works, and so does the full `Change-Id` value. A commit without a Change-Id — made with plain `git commit`, or cherry-picked from elsewhere — falls back to a hex prefix of its hash, such as `3a`, which changes whenever the commit is rewritten. +A commit's short ID comes first on its line, in a fixed column, followed by the abbreviated hash, so ID and hash sit together at the head of the line. Commits that carry a `Change-Id` trailer (every commit loom creates, see [`loom.changeId`](../configuration.md#loomchangeid)) get a **persistent** ID made of the letters `k`–`z`, such as `mqt`: it is derived from the Change-Id, not from the hash, so it survives `update`, `fold`, `swap`, `split`, and every other rewrite. Any longer prefix of the ID also works, and so does the full `Change-Id` value. A commit without a Change-Id — made with plain `git commit`, or cherry-picked from elsewhere — falls back to a hex prefix of its hash, such as `3a`, which changes whenever the commit is rewritten. IDs are the shortest prefix that tells commits apart. When a new commit happens to share the first letters of an existing one, both IDs grow by a letter and the old shorter form stops resolving, so a stale ID can never point at the wrong commit. @@ -95,7 +95,7 @@ git loom status -f ``` │╭─ fa [feature-a] -│● mqt Add feature A 2ee61e1 +│● mqt 2ee61e1 Add feature A │┊ mqt:0 M src/feature.rs │┊ mqt:1 A tests/feature_test.rs ├╯ @@ -119,11 +119,11 @@ Each feature branch forks from the integration line independently: ``` │╭─ fb [feature-b] -│● mqt Fix bug in feature B d0472f9 +│● mqt d0472f9 Fix bug in feature B ├╯ │ │╭─ fa [feature-a] -│● rsv Add feature A 2ee61e1 +│● rsv 2ee61e1 Add feature A ├╯ ``` @@ -133,12 +133,12 @@ Feature-b is stacked on top of feature-a: ``` │╭─ fb [feature-b] -│● mqt Second commit on feature-b 4e046ab -│● pkz First commit on feature-b 0b85ca7 +│● mqt 4e046ab Second commit on feature-b +│● pkz 0b85ca7 First commit on feature-b ││ │├─ fa [feature-a] -│● rsv Second commit on feature-a caa87a9 -│● tqn First commit on feature-a 18faee8 +│● rsv caa87a9 Second commit on feature-a +│● tqn 18faee8 First commit on feature-a ├╯ ``` @@ -149,7 +149,7 @@ Multiple branches pointing to the same commit: ``` │╭─ fv [feature-a-v2] │├─ fa [feature-a] -│● rsv Add feature A 2ee61e1 +│● rsv 2ee61e1 Add feature A ├╯ ``` @@ -158,7 +158,7 @@ Multiple branches pointing to the same commit: When upstream has new commits beyond the common base: ``` -● mqt Fix typo abc1234 +● mqt abc1234 Fix typo │ │● [origin/main] ⏫ 3 new commits ├╯ 204e309 (common base) 2025-07-06 Merge pull request #10 diff --git a/docs/src/guides/amending.md b/docs/src/guides/amending.md index c0fcf1c..37f734d 100644 --- a/docs/src/guides/amending.md +++ b/docs/src/guides/amending.md @@ -7,8 +7,8 @@ You realize the login form is missing a CSRF token. You fix `src/auth.rs` and ch │ M src/auth.rs │ │╭─ fa [feature-auth] -│● pkz add password validation 81356bf -│● mqt add login form a337eda +│● pkz 81356bf add password validation +│● mqt a337eda add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit diff --git a/docs/src/guides/fixup.md b/docs/src/guides/fixup.md index 41f0673..8d48965 100644 --- a/docs/src/guides/fixup.md +++ b/docs/src/guides/fixup.md @@ -4,8 +4,8 @@ You have two commits on `feature-auth` and realize that `pkz` ("add password val ``` │╭─ fa [feature-auth] -│● pkz add password validation 2b48f49 -│● mqt add login form bf7e5af +│● pkz 2b48f49 add password validation +│● mqt bf7e5af add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit @@ -21,7 +21,7 @@ Commit `pkz` disappears from history and its changes are absorbed into `mqt`: ``` │╭─ fa [feature-auth] -│● mqt add login form 9dff7bd +│● mqt 9dff7bd add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit diff --git a/docs/src/guides/moving-commits.md b/docs/src/guides/moving-commits.md index 9e15db2..4ce304d 100644 --- a/docs/src/guides/moving-commits.md +++ b/docs/src/guides/moving-commits.md @@ -4,12 +4,12 @@ You committed a logging helper to `feature-auth` by mistake — it belongs in `f ``` │╭─ fd [feature-dashboard] -│● rsv add dashboard layout 24a86e6 +│● rsv 24a86e6 add dashboard layout ├╯ │ │╭─ fa [feature-auth] -│● tqn add logging helper 6395f01 -│● mqt add login form 64518a4 +│● tqn 6395f01 add logging helper +│● mqt 64518a4 add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit @@ -25,12 +25,12 @@ Commit `tqn` is removed from `feature-auth` and appended to `feature-dashboard`: ``` │╭─ fd [feature-dashboard] -│● tqn add logging helper db04256 -│● rsv add dashboard layout 24a86e6 +│● tqn db04256 add logging helper +│● rsv 24a86e6 add dashboard layout ├╯ │ │╭─ fa [feature-auth] -│● mqt add login form 64518a4 +│● mqt 64518a4 add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit diff --git a/docs/src/guides/moving-files.md b/docs/src/guides/moving-files.md index 776d514..e0d8bc7 100644 --- a/docs/src/guides/moving-files.md +++ b/docs/src/guides/moving-files.md @@ -13,13 +13,13 @@ $ git loom status -f ``` │╭─ fd [feature-dashboard] -│● rsv add dashboard layout 147aa31 +│● rsv 147aa31 add dashboard layout │┊ rsv:0 A src/dashboard.rs │┊ rsv:1 A templates/dashboard.html ├╯ │ │╭─ fa [feature-auth] -│● mqt add login form c32bc09 +│● mqt c32bc09 add login form │┊ mqt:0 M src/auth.rs │┊ mqt:1 A templates/login.html ├╯ @@ -37,14 +37,14 @@ The file's changes are removed from `mqt` and applied to `rsv`: ``` │╭─ fd [feature-dashboard] -│● rsv add dashboard layout a3b2ef8 +│● rsv a3b2ef8 add dashboard layout │┊ rsv:0 A src/dashboard.rs │┊ rsv:1 A templates/dashboard.html │┊ rsv:2 A templates/login.html ├╯ │ │╭─ fa [feature-auth] -│● mqt add login form cb15064 +│● mqt cb15064 add login form │┊ mqt:0 M src/auth.rs ├╯ │ diff --git a/docs/src/guides/splitting.md b/docs/src/guides/splitting.md index 4d8a475..ffbb71c 100644 --- a/docs/src/guides/splitting.md +++ b/docs/src/guides/splitting.md @@ -8,7 +8,7 @@ git loom status -f mqt ``` │╭─ fa [feature-auth] -│● mqt add login form 291658f +│● mqt 291658f add login form │┊ mqt:0 A src/auth.rs │┊ mqt:1 A src/validation.rs │┊ mqt:2 A templates/login.html @@ -31,8 +31,8 @@ Select the files for the **first** commit — the remaining files stay in the ** ``` │╭─ fa [feature-auth] -│● mqt add login form 5e1a9c2 -│● wsl add validation helpers 8c2b2fa +│● mqt 5e1a9c2 add login form +│● wsl 8c2b2fa add validation helpers ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit diff --git a/docs/src/guides/tutorial.md b/docs/src/guides/tutorial.md index 03c6344..7df2705 100644 --- a/docs/src/guides/tutorial.md +++ b/docs/src/guides/tutorial.md @@ -50,7 +50,7 @@ Check the status: ``` │╭─ fa [feature-auth] -│● mqt add login form 711d082 +│● mqt 711d082 add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit @@ -68,8 +68,8 @@ Notice you can use the short ID `fa` instead of the full branch name. The status ``` │╭─ fa [feature-auth] -│● pkz add password validation 4f825d2 -│● mqt add login form 711d082 +│● pkz 4f825d2 add password validation +│● mqt 711d082 add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit @@ -85,8 +85,8 @@ While `feature-auth` is in progress, you want to start on a dashboard. You creat │ ⁕ templates/dashboard.html │ │╭─ fa [feature-auth] -│● pkz add password validation 4f825d2 -│● mqt add login form 711d082 +│● pkz 4f825d2 add password validation +│● mqt 711d082 add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit @@ -100,12 +100,12 @@ git loom commit -b feature-dashboard -m "add dashboard layout" zz ``` │╭─ fd [feature-dashboard] -│● rsv add dashboard layout 7a5bb0b +│● rsv 7a5bb0b add dashboard layout ├╯ │ │╭─ fa [feature-auth] -│● pkz add password validation 4f825d2 -│● mqt add login form 711d082 +│● pkz 4f825d2 add password validation +│● mqt 711d082 add login form ├╯ │ ● a1b2c3d (upstream) [origin/main] Latest upstream commit @@ -125,12 +125,12 @@ This fetches upstream changes, rebases your integration branch (including all wo ``` │╭─ fd [feature-dashboard] -│● rsv add dashboard layout 8f74d1c +│● rsv 8f74d1c add dashboard layout ├╯ │ │╭─ fa [feature-auth] -│● pkz add password validation 660e970 -│● mqt add login form 885cc0c +│● pkz 660e970 add password validation +│● mqt 885cc0c add login form ├╯ │ ● b2c3d4e (upstream) [origin/main] Teammate's latest commit diff --git a/docs/src/guides/uncommitting.md b/docs/src/guides/uncommitting.md index 35985e8..690c61c 100644 --- a/docs/src/guides/uncommitting.md +++ b/docs/src/guides/uncommitting.md @@ -8,9 +8,9 @@ $ git loom status -f ``` │╭─ fa [feature-auth] -│● pkz add password validation 92b8427 +│● pkz 92b8427 add password validation │┊ pkz:0 M src/auth.rs -│● mqt add login form bae0b72 +│● mqt bae0b72 add login form │┊ mqt:0 A src/auth.rs │┊ mqt:1 A templates/login.html ├╯ @@ -33,7 +33,7 @@ The commit is removed from history and its changes appear as unstaged modificati │ M src/auth.rs │ │╭─ fa [feature-auth] -│● mqt add login form bae0b72 +│● mqt bae0b72 add login form │┊ mqt:0 A src/auth.rs │┊ mqt:1 A templates/login.html ├╯ @@ -56,9 +56,9 @@ The file is removed from the commit and appears as an untracked file in the work │ ⁕ templates/login.html │ │╭─ fa [feature-auth] -│● pkz add password validation 2a660a7 +│● pkz 2a660a7 add password validation │┊ pkz:0 M src/auth.rs -│● mqt add login form 4afdd0c +│● mqt 4afdd0c add login form │┊ mqt:0 A src/auth.rs ├╯ │ diff --git a/specs/001-status.md b/specs/001-status.md index 6ebbc42..69c1584 100644 --- a/specs/001-status.md +++ b/specs/001-status.md @@ -26,7 +26,7 @@ Output runs top-to-bottom in this order: 4. Upstream or common-base marker. 5. Optional context commits. -Commits use ` `: the commit's short ID (Spec 002) in the fixed column defined by Spec 002, then the subject, then the abbreviated hash at the end of the line. Hashes are unique abbreviations respecting `core.abbrev`. Merge commits have no special treatment. +Commits use ` `: the commit's short ID (Spec 002) in the fixed column defined by Spec 002, then the abbreviated hash, then the subject. Hashes are unique abbreviations respecting `core.abbrev`. Merge commits have no special treatment. | Symbol | Normative meaning | | --- | --- | @@ -81,9 +81,9 @@ Minimal topology examples: ```text │╭─ [feature-b] │╭─ [feature-a-v2] │╭─ [feature-stale] -│● pkz B bbbbbbb │├─ [feature-a] ├╯ -│├─ [feature-a] │● mqt A aaaaaaa │ -│● mqt A aaaaaaa ├╯ ● base (upstream) [origin/main] +│● pkz bbbbbbb B │├─ [feature-a] ├╯ +│├─ [feature-a] │● mqt aaaaaaa A │ +│● mqt aaaaaaa A ├╯ ● base (upstream) [origin/main] ├╯ ``` diff --git a/specs/002-shortid.md b/specs/002-shortid.md index c990eea..52d0a4c 100644 --- a/specs/002-shortid.md +++ b/specs/002-shortid.md @@ -48,14 +48,14 @@ IDs use blue underline (`COLOR_SHORTID`). Placement is: │ ma M src/main.rs │ │╭─ fa [feature-a] -│● mqt Fix bug d072f9a -│● 3a Cherry-picked 3a6f21c +│● mqt d072f9a Fix bug +│● 3a 3a6f21c Cherry-picked ├╯ ``` -A commit line is ` `: the ID first, then the subject, then the dimmed abbreviated hash at the end of the line. ANSI-stripped output still contains the full abbreviated hash. The upstream/common-base marker gets no ID. +A commit line is ` `: the ID first, then the dimmed abbreviated hash, then the subject, so ID and hash sit together where both are easy to copy. ANSI-stripped output still contains the full abbreviated hash. The upstream/common-base marker gets no ID. -The ID occupies a fixed four-column field followed by one space, so the subject column does not move between invocations when a new commit lengthens an ID. An ID wider than the field (five letters, or a numeric-suffix fallback) keeps its single trailing space and shifts only its own line. The interactive TUI uses the same column but shows no hash. +The ID occupies a fixed four-column field followed by one space, so the hash and subject columns do not move between invocations when a new commit lengthens an ID. An ID wider than the field (five letters, or a numeric-suffix fallback) keeps its single trailing space and shifts only its own line. The interactive TUI uses the same column but shows no hash. ## Persistent commit identity diff --git a/src/core/graph.rs b/src/core/graph.rs index 0333ba6..d714bb6 100644 --- a/src/core/graph.rs +++ b/src/core/graph.rs @@ -449,7 +449,8 @@ pub(crate) fn is_stacked_with_next(sections: &[Section], idx: usize) -> bool { /// right rather than the whole tree. const COMMIT_ID_SLOT: usize = 4; -/// Spaces between a commit short ID and the subject: always at least one. +/// Spaces between a commit short ID and the hash that follows it: always at +/// least one. pub(crate) fn id_pad(sid: &str) -> String { " ".repeat(COMMIT_ID_SLOT.saturating_sub(sid.chars().count()) + 1) } @@ -726,8 +727,8 @@ fn render_branch( "●".color(dot_color), sid.color(theme.shortid).underline(), id_pad(sid), + commit.short_id.color(theme.dim), commit.message, - commit.short_id.color(theme.dim) ) .unwrap(); for (i, file) in commit.files.iter().enumerate() { @@ -771,8 +772,8 @@ fn render_loose( "●".color(theme.graph), sid.color(theme.shortid).underline(), id_pad(sid), + commit.short_id.color(theme.dim), commit.message, - commit.short_id.color(theme.dim) ) .unwrap(); for (i, file) in commit.files.iter().enumerate() { diff --git a/src/core/graph_test.rs b/src/core/graph_test.rs index 57e2e97..5ceb0ec 100644 --- a/src/core/graph_test.rs +++ b/src/core/graph_test.rs @@ -158,8 +158,8 @@ fn single_branch() { │ no changes │ │╭─ fa [feature-a] -│● 02 A2 0000002 -│● 01 A1 0000001 +│● 02 0000002 A2 +│● 01 0000001 A1 ├╯ │ ● aaa0000 (upstream) [origin/main] Initial commit @@ -246,7 +246,7 @@ fn loose_commits_on_integration_line() { let output = render_plain(info); assert!( - output.contains("● 02 Fix typo 0000002\n● 01 Refactor 0000001"), + output.contains("● 02 0000002 Fix typo\n● 01 0000001 Refactor"), "expected loose commits, got:\n{}", output ); @@ -277,7 +277,7 @@ fn mixed_loose_and_branch() { let output = render_plain(info); // Loose commit should appear before the branch assert!( - output.contains("● 03 Loose on top 0000003\n│\n│╭─ fb [feature-b]"), + output.contains("● 03 0000003 Loose on top\n│\n│╭─ fb [feature-b]"), "expected loose then branch, got:\n{}", output ); @@ -359,7 +359,7 @@ fn merge_based_integration_branch() { // Integration-line commits should be loose (plain ● without │ prefix) assert!( - output.contains("● 23 Feature 3 depends on Feature 2 0000023"), + output.contains("● 23 0000023 Feature 3 depends on Feature 2"), "expected loose integration commit, got:\n{}", output ); @@ -538,13 +538,13 @@ fn files_shown_under_branch_commits() { // File shortids use commit_sid:index format assert!( output.contains( - "│● 02 A2 0000002\n│┊ 02:0 M src/graph.rs\n│┊ 02:1 A new_file.txt\n" + "│● 02 0000002 A2\n│┊ 02:0 M src/graph.rs\n│┊ 02:1 A new_file.txt\n" ), "expected files under A2, got:\n{}", output ); assert!( - output.contains("│● 01 A1 0000001\n│┊ 01:0 M src/status.rs\n"), + output.contains("│● 01 0000001 A1\n│┊ 01:0 M src/status.rs\n"), "expected files under A1, got:\n{}", output ); @@ -567,7 +567,7 @@ fn files_shown_under_loose_commits() { let output = render_plain(info); // Loose commit file should have ┊ prefix with commit_sid:index format assert!( - output.contains("● 02 Fix typo 0000002\n┊ 02:0 M README.md\n"), + output.contains("● 02 0000002 Fix typo\n┊ 02:0 M README.md\n"), "expected files under loose commit, got:\n{}", output ); @@ -629,7 +629,7 @@ fn root_commit_files_shown() { let output = render_plain(info); assert!( - output.contains("● 01 Initial 0000001\n┊ 01:0 A init.rs\n"), + output.contains("● 01 0000001 Initial\n┊ 01:0 A init.rs\n"), "expected file under root commit, got:\n{}", output ); @@ -1186,7 +1186,7 @@ fn commit_ids_share_one_fixed_width_column() { let output = render_plain(info); assert!( - output.contains("│● wpn A2 0000002\n│● 01 A1 0000001\n"), + output.contains("│● wpn 0000002 A2\n│● 01 0000001 A1\n"), "{output}" ); } @@ -1210,9 +1210,9 @@ fn an_oversized_commit_id_pushes_only_its_own_line() { let output = render_plain(info); assert!( output.contains( - "│● wpnsz A3 0000003 -│● wpnsy A2 0000002 -│● 01 A1 0000001 + "│● wpnsz 0000003 A3 +│● wpnsy 0000002 A2 +│● 01 0000001 A1 " ), "{output}" @@ -1220,12 +1220,12 @@ fn an_oversized_commit_id_pushes_only_its_own_line() { } #[test] -fn loose_commit_keeps_the_hash_at_the_end_of_the_line() { +fn loose_commit_puts_the_hash_before_the_subject() { let mut info = base_info(); let mut c = commit(2, "Fix typo", Some(1)); c.change_id = Some("Ib710000000000000000000000000000000000000".to_string()); info.commits = vec![c]; let output = render_plain(info); - assert!(output.contains("● osy Fix typo 0000002\n"), "{output}"); + assert!(output.contains("● osy 0000002 Fix typo\n"), "{output}"); } diff --git a/tests/integration/helpers.sh b/tests/integration/helpers.sh index 8c44ddb..d765caf 100644 --- a/tests/integration/helpers.sh +++ b/tests/integration/helpers.sh @@ -170,12 +170,12 @@ head_parent_count() { git -C "$WORK" log -1 --pretty=%P | wc -w | t parent_count_at() { git -C "$WORK" log -1 "$1" --pretty=%P | wc -w | tr -d ' '; } # Return the short ID for a commit, given its message as shown in gl status: -# the field after the graph prefix (`│● mqt Message d072f9a`). +# the field after the graph prefix (`│● mqt d072f9a Message`). # Usage: commit_sid=$(commit_sid_from_status "Commit message") commit_sid_from_status() { gl status | subject="$1" awk ' ($1 == "●" || $1 == "│●") && - $NF ~ /^[0-9a-f]{7,}$/ && index($0, ENVIRON["subject"]) { + $3 ~ /^[0-9a-f]{7,}$/ && index($0, ENVIRON["subject"]) { print $2 exit }'