- T002 P0 feat - Add git2 dependency to Cargo.toml
- T003 P0 feat - Parse single CLI argument (commit-ish string)
- T004 P0 feat - Open repo from current directory with git2
- T005 P0 feat - Resolve CLI arg to Oid using revparse_single
- T006 P0 feat - Get HEAD as Oid
- T007 P0 feat - Call merge_base to find common ancestor
- T008 P0 feat - Print reference commit hash to stdout
- T009 P1 feat - Add integration test with TempDir fixture repo
- T010 P1 feat - Test resolving branch name to ref point
- T011 P1 feat - Test resolving tag to ref point
- T012 P1 feat - Test resolving short hash to ref point
- T013 P1 feat - Test resolving long hash to ref point
-
T014 P0 feat - Add ratatui and crossterm dependencies to Cargo.toml
-
T015 P0 feat - Create CommitInfo domain type (oid, summary, author, date) in lib.rs
-
T016 P0 feat - Implement list_commits(from_oid, to_oid) in library to get commits in range
-
T017 P0 feat - Create app module (src/app.rs) with AppState struct (Flags:
-
T018 P0 feat - Add commit list and selection index to AppState
-
T019 P0 feat - Implement methods for moving selection up/down in AppState
-
T020 P0 feat - Create event module (src/event.rs) for input handling
-
T021 P0 feat - Parse arrow keys and 'q' key in event module
-
T022 P0 feat - Create views module (src/views.rs) declaring commit_list submodule
-
T023 P0 feat - Create commit_list view (src/views/commit_list.rs) with render function
-
T024 P0 feat - Render table with "SHA" and "Title" column headers (Flags:
-
T025 P0 feat - Render commits oldest-to-newest with short SHA (7 chars) and summary
-
T026 P0 feat - Highlight selected row with different color/style (Flags:
-
T027 P0 feat - Update main.rs to initialize terminal with crossterm backend
-
T028 P0 feat - Implement main event loop: draw, handle input, update state
-
T029 P0 feat - Call list_commits with HEAD and reference point from CLI arg
-
T030 P0 feat - Handle 'q' key to exit and restore terminal
-
T031 P1 feat - Add integration test for list_commits returning correct order
-
T032 P1 feat - Add unit test for AppState selection movement
-
T033 P2 feat - Add TUI snapshot test with TestBackend for commit_list view
-
T035 P1 feat - Start application with HEAD commit selected instead of first commit
-
T036 P2 feat - Highlight table column headers with background color or style
-
T037 P1 feat - Make commit list scrollable when commits exceed screen height
-
T038 P1 feat - Render scrollbar for commit list when content exceeds visible area
-
T039 P1 feat - Add footer showing selected commit info (long SHA, commit position)
-
T040 P1 feat - Add clap dependency for CLI argument parsing
-
T041 P1 feat - Add --reverse flag to display commits in reverse order
-
T043 P2 feat - Remove Commits border from commit list table
-
T044 P0 feat - Add diff domain types: FileDiff, Hunk, DiffLine, CommitDiff
-
T045 P0 feat - Add commit_diff(oid) function in repo.rs using git2 to extract CommitDiff for a single commit
-
T046 P1 feat - Add integration tests for commit_diff using fixture repos
- T047 P0 feat - Add FileSpan type and extract_spans function in fragmap module
- T048 P1 feat - Add unit tests for span extraction
- T049 P0 feat - Build fragmap matrix: commits x chunks with TouchKind cells, one column per hunk
- T050 P1 feat - Add unit tests for matrix generation with fabricated CommitDiff data
- T051 P0 feat - Determine squashability between commit pairs sharing a column: yellow if trivial, red if conflicting
- T052 P1 feat - Add unit tests for squashability logic
-
T053 P0 feat - Compute fragmap data in main.rs and store in AppState
-
T054 P0 feat - Render fragmap grid right of commit title: white squares for touched chunks, colored lines between related commits
-
T055 P1 feat - Add snapshot tests for fragmap grid rendering
-
T056 P2 feat - Horizontal scrolling for fragmap columns exceeding available width
-
T057 P2 feat - Add horizontal scrollbar indicator for fragmap matrix
-
T058 P1 feat - Align fragmap matrix to the left, adjacent to title column
-
T059 P1 feat - Colorize SHA and title of commits where all touched clusters are squashable into the same single other commit
-
T060 P1 feat - Highlight related commits when a commit is selected: color SHA and title of squashable targets in yellow (COLOR_SQUASHABLE) and conflicting commits in red (COLOR_CONFLICTING), matching the vertical connector line colors
- T042 P0 bug - Commit list shows commits from repo start to reference point instead of from HEAD to reference point
- T034 P2 feat - Move find_reference_point and list_commits from lib.rs to repo module
-
T120 P2 fix - "Hunk groups" header label is truncated when the fragmap matrix has fewer columns than the label is wide: in
build_constraintsthe third column usesConstraint::Length(layout.fragmap_col_width), which clips the 11-character label to as few characters as there are cluster columns; fix by usingConstraint::Min(layout.fragmap_col_width)(orConstraint::Length(layout.fragmap_col_width.max(MIN_HEADER_WIDTH))) for the fragmap column so the header always has enough room to display the full label -
T121 P2 fix - Help dialog wraps long key-binding lines mid-text, splitting a single entry across two rows without indentation — making it hard to read; find the help text rendering in
views/help.rsand ensure each entry either fits on one line or wraps with a hanging indent (e.g. align continuation lines under the description column) so no entry appears to be two separate bindings -
T122 P2 fix - Dialogs that show multi-line body text (e.g. the "some conflicts are still unresolved" conflict dialog and similar) wrap long lines without preserving indentation: continuation lines start at column 0 inside the dialog instead of aligning with the start of the text on the first line; update
render_centered_dialog(or the individual dialog callers) to apply a hanging indent when wrapping body lines, so wrapped text is visually grouped under its first line -
T119 P1 fix - Handle Ctrl+C gracefully: always quit the application immediately regardless of the current mode; if the app is in
RebaseConflictmode (i.e. a rebase is in progress with a half-applied working tree), callrebase_abortfirst to restore the branch to its original state before exiting, so the repo is never left in a broken state; for all other modes (including selection overlays like SquashSelect, MoveSelect, SplitSelect — none of which have touched the repo yet) simply quit directly; parseKeyCode::Char('c')withKeyModifiers::CONTROLinAppMode::parse_key, map it to a newKeyCommand::ForceQuit, and handle it inmain.rsoutside the per-mode dispatch so it cannot be shadowed; ensure raw mode and the alternate screen are properly restored before exit -
T117 P2 feat - Allow the user to move the vertical separator bar between the commit list and the right panel (fragmap / commit detail) using Ctrl+Left and Ctrl+Right arrow keys; store the offset as a signed integer in
AppState(e.g.split_offset: i16) defaulting to 0, clamp it so both sides keep a minimum width, parseCtrl+Left/Ctrl+RightinAppMode::parse_keyas newKeyCommandvariants (SplitLeft/SplitRight), and apply the offset to thesplit_xconstant inrender_main_view
- T081 P0 feat - Exclude the reference point (merge-base) commit from the commit list and all operations — it is shared with the target branch and must not be squashed, moved, or split
- T061 P0 feat - Change exit key from 'q' to Esc
- T062 P1 feat - Add vertical separator line between title column and hunk groups column
- T063 P1 feat - Add help dialog on 'h' key showing all interactive keybindings (q=quit, i=info, s=split, m=move, h=help)
- T085 P2 feat - Add 'r' key to reload: re-read the commit list from HEAD down to the originally calculated reference point (merge-base), refreshing after external git operations without restarting the tool
- T086 P2 feat - Show staged and unstaged working-tree changes as synthetic rows at the top of the commit list (above HEAD), displayed with distinct labels ("staged" / "unstaged") and included in the fragmap matrix so their hunk overlap with commits is visible
-
T082 P1 feat - Improve selected row highlighting in the hunk group matrix; the current inverse-color style is hard to read — use a subtler approach such as a bold/bright foreground, a dim background tint, or a side marker (Flags:
-
T083 P2 feat - Add CLI flag
--no-dedup-columns(or similar) to disable deduplication of identical hunk-group columns in the fragmap view, useful for debugging and understanding the raw cluster layout
-
T064a P0 feat - Add DetailView app mode and 'i' key toggle, create basic commit_detail view module with placeholder rendering
-
T064b P0 feat - Display commit metadata in detail view: full message, author name, author date, commit date
-
T064c P0 feat - Add file list showing changed/added/removed files with status indicators
-
T064d P0 feat - Add complete diff rendering with +/- lines (plain text, no colors)
-
T065 P1 feat - Color diff output in commit detail view similar to tig: green for additions, red for deletions, cyan for hunk headers
-
T066 P1 feat - Support scrolling in commit detail view for long diffs
-
T067 P1 feat - Pressing 'i' again or Esc in detail view returns to the commit list with hunk groups
- T068 P0 feat - Add split mode on 's' key: prompt user to choose split strategy — one commit per file, per hunk, or per hunk cluster
- T069 P0 feat - Implement per-file split: create N commits each applying one file's changes, using git2 cherry-pick/tree manipulation; refuse if staged/unstaged changes overlap (share file paths) with the commit being split, and report the conflicting file(s) to the user
- T070 P1 feat - Implement per-hunk split: create one commit per hunk using git2 diff apply with filtered patches
- T071 P1 feat - Implement per-hunk-cluster split: create one commit per fragmap cluster column
- T072 P1 feat - Add numbering n/total to split commit messages in the subject line
- T087 P2 feat - Before executing a split that would produce more than 5 new commits, show a yes/no confirmation dialog displaying the count and asking the user to confirm before proceeding
-
T084a P1 feat - Implement
drop_commitonGitRepotrait: remove the selected commit by cherry-picking its descendants onto its parent. Return aRebaseOutcomethat is eitherCompleteon success orConflictwith enough state to resume or abort. Each cherry-pick step can conflict, so conflicts must be detected at every stage of the rebase. -
T084b P1 feat - Implement
drop_commit_continueanddrop_commit_abortonGitRepotrait: after the user resolves conflicts in the working tree,continuestages the resolution and resumes cherry-picking the remaining descendants;abortrestores the branch to its original state. -
T084c P1 feat - Wire drop to 'd' key in the TUI: always prompt the user for confirmation before executing (Enter to confirm, Esc to cancel). (Flags:
-
T084d P1 feat - Handle conflict during drop: when
drop_commitreturns a conflict, prompt the user to resolve it in their working tree (Enter to continue as resolved, Esc to abort the drop). -
T092 P2 fix - Wrap long commit summaries in the drop confirm and drop conflict dialogs so the title is never truncated when it exceeds the dialog width
-
T093 P2 feat - Show conflicting file paths in the drop conflict dialog: query the index for entries with conflict stage > 0 and list them inside the dialog so the user can see which files need to be resolved
-
T094 P1 fix - When
drop_commit_continueis called with partially unresolved conflicts (some files still have conflict markers), detect the remaining conflicts, show them to the user inside the dialog, and keep theDropConflictmode active instead of returning an error and leaving the repo in a broken state -
T095 P2 feat - When a merge conflict occurs during drop, offer to launch the user's configured merge tool (from
merge.tool/mergetool.<name>.cmdgit config) on each conflicted file. Suspend the TUI (disable raw mode, leave alternate screen), write the three index stages (base/ours/theirs) to temp files, invoke the tool and wait for it to exit (same contract as the commit message editor), then restore the TUI and re-read the index to refreshconflicting_files. If no merge tool is configured, leave the current behavior unchanged.
- T073 P0 feat - Add move mode on 'm' key: highlight selected commit and
show a "move here" insertion row navigable with arrow keys.
Design: move
KeyCommandenum and key parsing intoapp.rs, implementAppMode::parse_key(event: Event) -> KeyCommandso each mode resolves ambiguous keys ('m' →MoveCommitinCommitList,MergetoolinRebaseConflict), and delete theeventmodule. UI: addAppMode::MoveCommit { source_index: usize, insert_before: usize };build_rowsinjects a styled separator row (e.g.▶ move here) at the insertion point — same pattern as the existing squash source highlight. A thin line between rows is not feasible with ratatui's Table widget without reimplementing layout. - [-] T074 P1 feat - Color the insertion row red with "move here - likely conflict" when moving to a position that would cause a conflict (Flags: WONT DO)
- T075 P0 feat - Execute the move via git2 cherry-pick rebase onto the new position, abort and notify user on conflict
- T076 P2 feat - On conflict, tell the user whether the conflict is in the moved commit or in a commit rebased on top of it
-
T099 P1 feat - Generalize conflict handling for reuse by squash and future operations: rename
drop_commit_continue/drop_commit_abort→rebase_continue/rebase_aborton theGitRepotrait andGit2Repoimpl, renameAppAction::ContinueDrop/AbortDrop→RebaseContinue/RebaseAbort, renameAppMode::DropConflict→RebaseConflict, add anoperation_labelfield toConflictStateso the conflict dialog title and success messages reflect the originating operation ("Drop Conflict" vs "Squash Conflict"), extract conflict dialog code (handle_conflict_key,render_drop_conflict) fromviews/drop.rsinto a newviews/conflict.rs, and update all references inmain.rs,app.rs,AppMode::background(), tests, and help text (Flags: -
T101 P1 feat - Remap split key from 's' to 'p' (sPlit) in the commit list view and help dialog, freeing 's' for squash which matches git's interactive rebase keybindings
-
T077 P0 feat - Add squash mode on 's' key: enter a
SquashSelectapp mode where the selected commit is the "source" and the user navigates with arrow keys to pick a squash target; the source is squashed into the target (target keeps its position, source is removed, their changes are combined); pressing Enter confirms the target, Esc cancels back to CommitList; block the key when the selected row is a staged/unstaged synthetic entry -
T078 P1 feat - Color squash target candidates in SquashSelect mode: yellow if squashable without conflict, red if the squash would likely conflict (overlapping fragmap clusters), white/dim if unrelated (no shared hunks and no conflict)
-
T079 P0 feat - Implement
squash_commitson theGitRepotrait: given source and target OIDs plushead_oid, create a combined tree by cherry-picking the target then the source onto the target's parent, then cherry-pick all remaining descendants (commits between target and source exclusive, plus commits after source) onto the result usingcherry_pick_chain— returnRebaseOutcomeso conflicts during the descendant rebase are handled by the generalized conflict infrastructure -
T100 P0 feat - Wire squash execution in the TUI: after the user picks a target in SquashSelect, open the editor (reuse
edit_message_in_editor) with both commit messages concatenated — target message first, then a blank line, then source message, matching git's interactive-rebase squash format; if the user saves an unchanged or non-empty message, callsquash_commits; onRebaseOutcome::ConflictenterRebaseConflictmode (reusing the generalized conflict dialog, continue, abort, and mergetool flows from T099); on success reload commits and show a confirmation message -
T080 P2 feat - Handle squash-time conflict (source changes conflict with target changes): when creating the combined tree itself fails due to overlapping edits in the source and target commits, write the conflict to the working tree and enter
RebaseConflictmode so the user can resolve, continue, abort, or launch the mergetool — same flow as descendant rebase conflicts -
T102 P1 feat - Replace the SquashSelect overlay dialog with a footer-based context line: remove
squash_select::render()and its centered dialog, and instead show a footer message inrender_footerwhen in SquashSelect mode — e.g.Squash: select target for <short_oid> "<summary>" · Enter confirm · Esc cancel— so the commit list is never obscured while picking a squash target; the source commit's magenta highlight and candidate coloring already provide sufficient visual context -
T103 P1 feat - Restrict SquashSelect cursor to earlier commits only: in
squash_select::handle_key, clamp navigation so the cursor cannot move to commits later than (above) the source commit — squashing into a later commit is not supported; also dim the rows above the source in the commit list when in SquashSelect mode to visually indicate they are unreachable targets (Flags: -
T104 P1 feat - Add fixup mode on 'f' key: works identically to squash ('s') — enters
SquashSelect, uses the same target-picking UI, candidate coloring, and conflict handling — but instead of opening the editor with both messages concatenated, it silently keeps the target commit's message as-is (the source commit's message is discarded); reusesquash_try_combine,squash_commits, andsquash_finalizewith the target's message passed directly, skippingedit_message_in_editor; update the footer context line to say "Fixup" instead of "Squash" and add 'f' to the help dialog
- T088 P1 feat - Implement
resolve_editor()helper: walk GIT_EDITOR env var → core.editor git config → VISUAL env var → EDITOR env var → "vi" fallback, matching git's own editor resolution order - T089 P1 feat - Implement general
edit_message_in_editor(repo, message)utility: write message to a tempfile, suspend TUI (disable raw mode, leave alternate screen), spawn the resolved editor with inherited stdio and the tempfile as argument, wait for exit, restore TUI (enable raw mode, re-enter alternate screen), read and return the edited message; works for both terminal-UI editors (e.g.vim,emacs -nw) and GUI editors that open their own window (e.g.code --wait) — this function is intentionally general so it can be reused when editing commit messages during squash - T090 P1 feat - Change reload key from 'r' to 'u' (update) in commit list view and help dialog, to free 'r' for reword
- T091 P1 feat - Add 'r' reword key in commit list view: invoke
edit_message_in_editorwith the selected commit's message, then use git2 to recreate the commit with the same tree and parents but the new message; if the commit is not HEAD, cherry-pick all descendants onto the new commit chain (same approach as split) — no conflict risk since only the message changes and the tree content is identical at every step, so staged/unstaged working-tree changes are unaffected and do not need to block this operation; block the key (show an error) only when the selected row is a staged or unstaged synthetic entry
- T109 P2 feat - Add
--staticCLI flag to output the commit SHA/title list and fragmap matrix to stdout without launching the interactive TUI, mimicking the behavior of the original fragmap tool; format each row as: short SHA in cyan, commit title truncated to 26 chars (gray if the commit is fully squashable, normal otherwise), then one character per cluster column —.for empty, a white-background space (\x1b[47m \x1b[0m) for a direct hunk-group touch (regardless of squash status), a yellow-background space (\x1b[43m \x1b[0m) for a squashable connector between two touching commits, and a red-background space (\x1b[41m \x1b[0m) for a conflicting connector; skip staged/unstaged synthetic rows (not present in original fragmap output); then exit - T110 P3 feat - Add
--no-colorCLI flag to disable all color output when used with--staticfrom T109, producing plain text output suitable for piping or automated processing; ensure this works correctly with the fragmap symbols and commit list formatting
- T096 P1 feat - Refactor event loop to mode-first dispatch: flip the main
match from action-first to mode-first so there is one small match on
AppModedelegating to ahandle_action(action, app)function in each view module (co-located withrender()). Each handler returns anActionResultenum (Handled, ExecuteSplit, ExecuteDrop, Quit, etc.) so view modules stay free of git/terminal dependencies andmain.rsonly interprets the result - T097 P2 feat - Extract shared dialog rendering helper: create
views/dialog.rswith arender_centered_dialog(frame, config)utility that handles centering, clearing, bordering and wrapping — then refactor drop confirm, drop conflict, split select, split confirm and help dialogs to use it, eliminating the duplicated layout/clear/border code - T098 P2 feat - Formalize the overlay concept: add an
AppMode::background()method that returns the underlying mode to render first for overlay modes (SplitSelect, SplitConfirm, DropConfirm, DropConflict, Help), then simplify the render dispatch inmain.rsto callrender_mode(background)thenrender_mode(foreground)instead of hand-coding the layering for each overlay variant - T123 P2 feat - Extract render_main_view from main.rs into views/main_view.rs: move the split-panel orchestrator (separator clamping, left/right area computation, fragmap hide/restore, commit_list + commit_detail coordination) out of main.rs into a proper view module
- T124 P2 feat - Extract fragmap rendering helpers into views/hunk_groups.rs: move build_fragmap_cell, fragmap_cell_content, fragmap_connector_content, cluster_relation, commit_text_style, fragmap color constants, and render_horizontal_scrollbar out of commit_list.rs into a dedicated module. commit_list.rs calls into hunk_groups for the third table column
- T125 P3 feat - Move SeparatorLeft/Right handling out of main event loop:
instead of the event loop doing
if action == SeparatorLeft { ... continue; }, handle separator_offset mutation inside the view handle_key (main_view or commit_list), returning AppAction::Handled
-
T128 P2 feat - Adapt title column width to terminal width in
--staticoutput: the original fragmap tool sets the title column width dynamically so that the SHA + title + hunk-group matrix fills the available terminal width; investigate the original Python implementation (https://github.com/amollberg/fragmap) to understand the exact layout algorithm (how many columns it reserves for SHA, separators, and the matrix, and how it clamps the title width), then implement the same or equivalent logic instatic_views::fragmap::render— the title currently uses a fixed 26-character truncation; instead, detect the terminal width (viacrossterm::terminal::size()or a passed-in width, falling back to 80), computetitle_width = terminal_width − sha_width − separators − matrix_widthclamped to a sensible minimum, and truncate/pad the title to that width -
T126 P2 feat - Add
--squashable-scope <commit|group>CLI argument controlling what the squashable connector color/symbol means:group(default in TUI) — a connector in a column is squashable when that hunk-group pair alone has no intervening touches (current per-group behavior);commit(default in--static) — a connector is squashable only when the entire lower commit is fully squashable into the same single upper commit (i.e.fragmap.is_fully_squashable()is true andsquash_target()points to that upper commit), matching the original fragmap tool's stricter rule; the argument must be valid in both TUI and--staticmodes; store the choice inAppStateand thread it through the fragmap connector rendering logic in bothstatic_views::fragmap::renderand the TUI fragmap widget -
T127 P2 fix - Respect the
-r/--reverseflag when--staticis used: currently--staticalways outputs commits in the order returned bylist_commits(newest-first); when--reverseis also passed the rows should be printed oldest-first, matching the interactive TUI behavior -
T111 P3 feat - Replace the current example application in
examples/with a compatibility tool that takes a commit-ish as its argument, uses it to find the merge-base (same as--static), then builds aFragmapobject in the normal way and also runs the originalfragmapbinary (if installed) on the same repository/ref; the tool renders git-tailor's result through the static view and compares the two outputs column-by-column (columns may be in any order); if the same commit-cluster relationships are present in both it prints "OK"; otherwise it prints thefragmapoutput, then git-tailor's static output, plus a short summary explaining what differs
- T114 P2 feat - Write comprehensive README.md documentation: describe what the tool does (interactive git commit browser with fragmap visualization and rebase operations), installation instructions, basic usage guide with key bindings, attribution to original fragmap tool (reference NOTICE file), note that the entire tool is AI-generated, and include a prominent data safety disclaimer warning users to push their changes before using the tool since any bugs may cause permanent data loss — author takes no responsibility for data loss under any circumstances, see Apache 2.0 license text
- T115 P2 feat - Add CHANGELOG.md following keepachangelog.com format: create initial changelog with sections for Unreleased, version entries (Added, Changed, Deprecated, Removed, Fixed, Security), and update AGENTS.md to instruct AI agents to ask users whether changes should be noted in the changelog when completing tasks that add user-visible features or fix bugs
- T129 P1 bug - Fix move/drop/fixup/squash/split losing working-tree and
index changes: currently these rebase operations discard any uncommitted
changes (both staged and unstaged) that exist in the working tree when the
operation is applied;
rewordalready preserves them correctly, so audit howrewordsaves and restores the working-tree and index state and apply the same stash-and-restore (or equivalent) pattern tomove_commit,drop_commit,squash_commit,fixup_commit, andsplit_commitin the rebase engine; add integration tests in thetests/directory covering all five operations with both staged changes (files added to the index but not committed) and unstaged changes (modified tracked files not yet staged), asserting that after the operation completes the working tree and index reflect the same content that was present before the operation started (Flags:
- T130 P2 feat - Auto-detect the repository default branch when no
<BASE>is provided on the command line: resolveorigin/HEADviagit rev-parse --abbrev-ref origin/HEAD(libgit2: look up the symbolic target ofrefs/remotes/origin/HEAD) and use the resulting branch as the base; fall back to the current hard-coded default iforigin/HEADis not set.
- T136 P1 bug - Error messages disappear instantly on Windows: on Windows,
crossterm fires both a key-down and a key-release event for a single
keystroke; error messages shown after an invalid operation (e.g. attempting a
move or squash with unstaged changes) are dismissed immediately because the
key-release event is treated as the user acknowledgment key press, making the
message unreadable; filter out
KeyEventKind::Release(andKeyEventKind::Repeatif appropriate) events in the input handling layer so that onlyKeyEventKind::Pressevents are acted upon, matching the Linux behavior where only press events are emitted - T137 P2 bug - First commit always excluded when browsing complete history:
when the user passes the very first (root) commit of the repository as the
positional
baseargument, that commit is never shown in the commit list; the root cause is thatmain.rsalways filters out the reference-point commit (filter(|c| c.oid != reference_oid)) because in the normal branch-workflow the merge-base is shared history that should not be editable; for complete-repository history this invariant does not hold and the root commit must be included; the fix should detect the root-commit / no-parent case (or add an--allflag) to skip the exclusion filter so that all commits from HEAD down to and including the first commit are shown and can be reordered, squashed, or split; the rebase engine'sreference_oidconcept (the "parent" onto which cherry-picks land) also needs to handle the case where there is no parent commit — likely by cherry-picking onto an empty tree for the first commit in the new sequence
- T131 P1 bug - Fixup conflict resolution incorrectly opens commit message
editor: when a fixup operation causes a conflict in the squash tree itself and
the user resolves it,
RebaseContinueinmain.rsalways opens the editor for the commit message (viasquash_finalize) regardless of whether the operation was a squash or a fixup; theSquashContextneeds anis_fixupfield (or equivalent) so that the editor is skipped and the target message is used as-is when finalizing a fixup, mirroring the non-conflict path inPrepareSquash - T132 P1 bug - Fixup conflict falsely reported as still unresolved: after
the user resolves a conflict during a fixup (either manually or via mergetool)
and presses Enter to continue,
rebase_continueingit2_impl.rsre-reads the index withindex.read(true)and callsindex.has_conflicts(), which returns true even though the working-tree file has been correctly resolved and staged; investigate whether libgit2's in-memory index is not being refreshed from disk before thehas_conflicts()check, or whether deleted-file conflicts leave behind phantom stage entries, and fix so that a genuinely resolved index is not incorrectly treated as unresolved - T133 P1 bug - Aborting a fixup after a conflict leaves dirty working tree:
rebase_abortingit2_impl.rsresets the branch ref and callscheckout_head(), but this does not clean up untracked files or staged deletions that were left behind by the failed cherry-pick (e.g. a file that was deleted in the conflict appears as a staged deletion and also as an untracked file after the abort); the abort should additionally clean untracked files and reset the index so the working tree matches HEAD, similar to whatgit checkout -f HEADfollowed bygit clean -fdwould do (fixed by T130: libgit2'scheckout_head(force)already resets both the index and workdir to HEAD, including files absent from HEAD's tree; the dirty-workdir symptom was a consequence of T130'sstage_filebug leaving the index in a corrupt state; integration test added to confirm) - T134 P1 bug - External editor conflict resolution not detected during squash/fixup: when a conflict occurs during squash or fixup and the user resolves it by editing the conflicted file in an external editor (e.g. VS Code) and saving, git-tailor does not detect the resolution; opening the built-in mergetool afterward still shows the original conflict markers as if the external edits were ignored; resolving via the built-in mergetool works correctly; the likely cause is that git-tailor reads the file content from git2's in-memory state or a cached copy rather than re-reading from the working tree on disk when checking conflict status or launching the mergetool
- T147 P1 bug - Segfault when splitting a submodule-change commit per file:
calling "split per file" on a commit that updates a submodule revision causes
a segfault; the split-per-file path in
git2_impl.rsiterates over the commit's diff entries and builds per-file patches usingDiff::apply_to_tree, but a submodule change produces a delta whose old/new objects are commit OIDs rather than blob OIDs; attempting to treat a submodule entry as a regular blob (e.g. passing it toBlob::lookupor building a patch from it) likely triggers a null dereference or invalid memory access inside libgit2; the fix should detect submodule deltas (delta kindGIT_DELTA_*where the object mode isGIT_FILEMODE_COMMIT, i.e.0o160000) and handle them explicitly — either by applying the submodule pointer update as a tree-level operation instead of a blob diff, or by grouping all submodule deltas into a single synthesised commit so the split result is well-formed; add an integration test using aTempDirrepo with a real submodule to reproduce the crash and verify the fix - T148 P2 bug - Split commits lose the original commit message body: all
three split strategies (per-file, per-hunk, per-hunk-group) construct the
message for each new commit using only
commit.summary()(the first line), appending a(n/total)counter; a commit whose message has a multi-line body or a detailed description will have that body silently discarded; the fix should usecommit.message()instead, replacing just the first line with the summary + counter so the full body is retained in all split commits (or at least in the last one, mirroring whatgit commit --amendandgit rebasedo by default); all threeformat!message expressions ingit2_impl.rsneed updating - T150 P2 bug - Splitting the root commit in
--allmode fails with "Can only split a commit with exactly one parent":split_commit_per_file,split_commit_per_hunk, andsplit_commit_per_hunk_groupingit2_impl.rsall reject commits withparent_count != 1; the fix should apply the same pattern used formove_commit— build the first split-piece commit as a new orphan root (applying its diff onto an empty tree with no parents), then cherry-pick the remaining split pieces and any later commits on top
- T149 P2 bug - Moving a commit to the earliest position places it second
instead of first: when using
gt --all(or any case where the oldest visible commit is also the root commit), selecting a commit and choosing to move it before the first commit in the list results in the commit being placed immediately after the root commit rather than before it; the status message reports success; the root cause is likely thatmove_commitingit2_impl.rsresolves the "insert before first commit" target as "insert after merge-base / root", but for--allthe root commit is included in the editable list which makes this the wrong reference point; the fix should ensure that when the target position is before the first commit, the entire cherry-pick chain is rebuilt with the root commit cherry-picked onto an empty tree first, the same way T137 handled the no-parent case for the initial rebase
- T135 P2 feat - Add option to open the configured editor when resolving a
conflict: the conflict view currently offers a key binding to launch the
mergetool (
core.mergetool/merge.tool); add a second key binding (e.g.e) that instead opens the conflicted file in the user's configured editor (core.editor, falling back to$VISUAL, then$EDITOR, then a sensible default such asvi); after the editor exits, re-check the file for conflict markers and update the conflict view state accordingly, the same way the mergetool path does
- T105 P2 feat - Add glyph-weight focus highlighting to the fragmap matrix:
clusters related to the focus commit (selected commit in CommitList, source
commit in SquashSelect/MoveSelect) use heavy glyphs —
█for touched squares and┃for connectors — while unrelated clusters use light glyphs —▪for touched squares and┆for connectors. Colors stay unchanged (white for conflicting squares, gray for squashable squares, red/yellow for connectors). This makes it immediately scannable which hunk groups the focus commit participates in without introducing new colors. "Related" means the cluster column contains a touch from the focus commit. Implement as aFocusThemebehind theFragmapThemetrait from T106. - T106 P2 feat - Refactor fragmap cell rendering into a
FragmapThemetrait with four methods keyed by two enums:SquareRole(Current= the focus commit's own square,Related= another commit's square in a focus-cluster column,Unrelated= any square in a non-focus-cluster column),ConnectorRole(Related= the column is a focus cluster,Unrelated= otherwise), andRelationType(Conflict|Squashable); the trait methods aresquare_symbol(SquareRole, RelationType) -> char,square_style(SquareRole, RelationType) -> Style,connector_symbol(ConnectorRole, RelationType) -> char, andconnector_style(ConnectorRole, RelationType) -> Style; implementPlainThemereproducing the current uniform heavy-glyph behavior (no focus distinction); replace the inline constant lookups infragmap_cell_content,fragmap_connector_content, andbuild_fragmap_cellwith calls through the trait so that adding new themes (T105, T107) doesn't require scattering conditionals throughout the rendering functions - T107 P3 feat - Add
--theme <THEME>CLI option to select the fragmap rendering theme; three themes are supported:plain(the current uniform heavy-glyph rendering with no focus-related highlighting, equivalent to DefaultTheme from T106),highlight(glyph-weight focus highlighting from T105 where clusters related to the selected commit use heavy glyphs and unrelated clusters use light glyphs), andclassic(identical rendering to--static, reproducing the traditional fragmap tool appearance); store the selected theme inAppStateand select the appropriateFragmapThemeimplementation at startup;plainshould be the default - T108 P1 fix - Fix fragmap relations not following file renames: when a
file is renamed across commits, spans should cluster together if they overlap
the same logical content, but currently they are treated as separate files and
don't form clusters. Investigate the original fragmap Python implementation
(https://github.com/amollberg/fragmap) to see how rename detection is handled
in span clustering, and adapt the SPG logic in
src/fragmap/spg.rsto properly track renamed files so that overlapping spans across renames are correctly clustered together
- T139 P3 feat - Add text search in commit detail view: add an incremental
search mode activated by
/(vim convention) that opens a search input bar at the bottom of the commit detail view; as the user types, highlight all matches in the visible diff content and scroll to the first match; supportn/Nto jump to next / previous match;Escapedismisses the search bar; the search should operate over the rendered diff text (file paths, hunk headers, and diff lines) and wrap around at the end of the content
- T112 P3 feat - Set up cargo-deny with configuration to check dependency
licenses are compatible with Apache 2.0: install cargo-deny, create
deny.tomlconfig allowing Apache-compatible licenses (Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause, ISC, etc.), deny copyleft licenses (GPL, LGPL, AGPL), and addcargo deny checkcommand to verify no license violations in the dependency tree - T113 P3 feat - Add cargo-deny to GitHub Actions CI: create or update
.github/workflows/ci.ymlto runcargo deny check licensesalongside existing format/clippy/test checks, failing the build if any dependency license conflicts are detected; ensure this runs on pull requests and main branch pushes
- T151 P3 fix - Eliminate duplication between
AppState::new()andAppState::with_commits(): both functions repeat the same ~30 field initializations verbatim; implementDefaultforAppStatecontaining all the zero-values, then havewith_commitsconstruct viaAppState { commits, selection_index, ..Default::default() }andnewdelegate toDefault; remove the duplicate field lists entirely - T152 P3 fix - Extract repeated
head_oidfetch pattern inmain.rs: the blockmatch git_repo.head_oid() { Ok(oid) => oid, Err(e) => { app.set_error_message(...); continue; } }appears five times in theAppActiondispatch arms (PrepareSplit, PrepareDropConfirm, PrepareReword, PrepareSquash, ExecuteMove); extract a local macro or inline helperget_head_oid!(git_repo, app)that encapsulates the error path so each call site is a single expression - T153 P3 fix - Add
CommitInfo::is_synthetic()helper to replace scattered inline checks: the expressioncommit.oid == "staged" || commit.oid == "unstaged"is repeated in five or more places acrossapp.rsandcommit_list.rs; add apub fn is_synthetic(&self) -> boolmethod toCommitInfoinlib.rsand replace every inline occurrence with a call to it - T154 P3 fix - Introduce
Oid/VirtualOidtypes: replace rawStringOIDs throughout the codebase with a newtypeOid(String)(short()/long() accessors, Display, From, From<&str>, Fromgit2::Oid) and aVirtualOidenum (Real(Oid),Staged,Unstaged) for commit-list entries that may be synthetic working-tree pseudo-commits; addCommitInfo::is_synthetic(),From<&Oid> for git2::Oid, and update all call sites, tests, and snapshots - T155 P3 fix - Extract common split-commit preamble into a shared helper:
split_commit_per_file,split_commit_per_hunk, andsplit_commit_per_hunk_groupingit2_impl.rseach begin with ~20 identical lines (parse OID, find commit, bail on merge commit, computeparent_treehandling the root-commit case, getcommit_tree); extract a private helperfn load_split_commit(repo, oid) -> Result<SplitCommitParts>returning the shared values, and apply the same extraction to the threecount_split_*methods which duplicate the same setup - T156 P3 fix - Remove redundant
visible_clustersdouble-iteration incompute_layout:commit_list.rs::compute_layoutiterates the fragmap matrix twice with identical predicate logic — once to computevisible_cluster_countfor the scrollbar decision, then again to buildvisible_clusters: Vec<usize>; compute theVecfirst and derive the count fromvisible_clusters.len()to eliminate the duplicate pass - T157 P3 fix - Split
src/repo/git2_impl.rs(2238 lines) into focused sub-modules undersrc/repo/git2_impl/:reads.rs(head_oid, list_commits, commit_diff, staged/unstaged_diff, default_branch, root_commit_oid, get_config_string),split.rs(the three split_commit_per_* methods + count_split_per_* + the load_split_commit helper from T155),squash.rs(squash_try_combine, squash_finalize, squash_commits),move_drop.rs(move_commit, drop_commit, reword_commit),conflict.rs(rebase_continue, rebase_abort, collect_conflict_files, write_conflicts_to_workdir, auto_stage_resolved_conflicts, read_conflicting_files), andhunks.rs(the pure free-function helpers: apply_single_hunk_to_tree, apply_hunk_to_content, apply_multiple_hunks_to_content, apply_selected_hunks_to_tree, apply_gitlink_delta_to_tree, split_lines_keep_eol); theGit2Repostruct stays ingit2_impl.rsand each sub-module adds itsimpl Git2Repo/impl GitRepo for Git2Repoblock; preserves the existing public API - T158 P3 fix - Move the inline
#[cfg(test)] mod testsblock (~1600 lines) out ofsrc/fragmap.rs(2387 lines) into a separatesrc/fragmap/tests.rsfile gated by#[cfg(test)] mod tests;infragmap.rs; production code drops to ~700 lines and the file becomes navigable; no behavioral change - T159 P2 fix - Extract
AppState::reload_preserving_selection(&impl GitRepo)to replace the five-times-repeated patternlet saved_index = app.selection_index; reload_commits(&git_repo, &mut app); app.selection_index = saved_index.min(app.commits.len().saturating_sub(1));inmain.rs(drop, move, squash, squash_finalize, rebase_continue); each call site becomes a single line - T160 P2 fix - Extract a
handle_rebase_outcomehelper (free fn or AppState method) inmain.rsto consolidate the repeatedmatch outcome { Ok(RebaseOutcome::Complete) => { reload + preserve_selection + set_success_message }, Ok(RebaseOutcome::Conflict(state)) => app.enter_rebase_conflict(*state), Err(e) => app.set_error_message(format! ("{label} failed: {e}")) }block; called by ExecuteDrop, ExecuteMove, PrepareSquash, RebaseContinue, and the squash finalize path; reduces ~80 lines of boilerplate - T161 P3 fix - Extract a
run_external_tool<T>(terminal, kb_enhanced, f)helper inmain.rsthat wraps thewith_external_process(kb_enhanced, f) + terminal.clear()?pattern; used by editor invocations (PrepareReword, squash-message editor, conflict-finalize editor) and the mergetool/editor conflict-resolution paths; the four current call sites collapse from 4 lines each to 1 - T162 P2 fix - Decompose
main.rs::main()(~530 lines) into focused helpers:load_initial_commits(&git_repo, &cli)returning(Vec<CommitInfo>, String, bool)(extracts lines 222–254),setup_terminal()returning a RAIITerminalGuardthat owns raw mode + alternate screen + keyboard enhancement and restores them on Drop (extracts lines 285–303 and 743–748),init_app_state(commits, &cli, &git_repo)returning the configuredAppStatewith synthetic rows (extracts lines 305–320), and split the giantAppActiondispatchmatchintodispatch_action(action, &mut app, &git_repo, terminal, kb_enhanced) -> Result<()>somainreads as a clear setup → loop → teardown flow under 50 lines - T163 P3 fix - Decompose
views/commit_detail.rs::render(~290 lines) into focused helpers:build_metadata_lines(commit) -> Vec<Line>(oid, message, author, dates),build_file_list_lines(diff) -> Vec<Line>(the "Changed Files:" section with status indicators),build_diff_lines(diff) -> Vec<Line>(file headers + hunk headers + colored +/- lines), andcompute_scroll_layout(content_area, content) -> ScrollLayout(returns text_area, scrollbar areas, max_scroll, max_h_scroll); render becomes a composition of these helpers + the search-highlight pass + widget calls - T164 P3 fix - Decompose
views/commit_list.rs::build_rows(~190 lines) by extractingfn row_text_style(app, focus_ctx: FocusContext, commit_idx, is_selected, is_synthetic) -> Styleto replace the 60-line nested if/else-if chain that picks the foreground style based on squash/move/normal mode; introduce a smallFocusContextenum (Squash { source_idx },Move { source_idx },Normal) to make the dispatch explicit; also addAppState::fragmap_index(visual_idx) -> usizeto remove the three repeatedif app.reverse { len-1-idx } else { idx }expressions in build_rows
- T190 P1 feat - Move duplicated
file_content_atandcommits_from_headhelpers intotests/common.rs: identical 8-line and 13-line definitions appear attests/split_commit.rs:20,tests/squash_commit.rs:23,tests/drop_commit.rs:23,tests/move_commit.rs:23(and the matchingcommits_from_headat:31/:34/:34/:34). Move both totests/common.rsaspub fn file_content_at(...)/pub fn commits_from_head(...)and remove the four local copies; ~80 LOC of duplication eliminated and ~80 call sites stay readable viacommon::file_content_at(...)/common::commits_from_head(...). - T191 P1 feat - Move duplicated
NoOpRepoGitRepo stub intotests/common.rs: thestruct NoOpRepoplus its ~120-lineGitRepoimpl (every methodunimplemented!()/panics) is defined identically attests/tui_main_view.rs:30andtests/tui_commit_detail.rs:33. Promote topub struct NoOpRepo;intests/common.rsand import from both files; eliminates ~120 LOC of risky copy-paste that has to stay in sync with theGitRepotrait. - T192 P1 feat - Add
assert_complete!/assert_conflict!macros forRebaseOutcome: the patternsassert!(matches!(result, RebaseOutcome::Complete), …)andmatch outcome { RebaseOutcome::Complete => panic!("expected conflict"), RebaseOutcome::Conflict(state) => *state }recur 40+ times acrosstests/{drop,squash,move}_commit.rsandtests/mergetool.rs. Add two macros totests/common.rs:assert_complete!(outcome)andexpect_conflict!(outcome) -> ConflictState(returns the boxed state, panicking otherwise). Call sites become one line each and read as intent rather than as a match-on-an-enum. - T193 P2 feat - Add
assert_history!(repo, base, &["msg1", "msg2"])helper: the pattern "walk commits from HEAD back to base, assert count, then per-commit assert summary contains/equals X" is repeated 15+ times acrosstests/{split,squash,drop,move}_commit.rswith bespoke loops. Add a helper intests/common.rs:pub fn assert_history(repo: &git2::Repository, base: git2::Oid, expected_summaries: &[&str])that verifies the count and each summary in oldest-to-newest order with descriptive panic messages. Each test then asserts the post-rebase commit graph in a single line. - T194 P2 feat - Add
assert_file_contents!macro: the patternassert_eq!(file_content_at(&test.repo, head_oid, "a.txt"), "alpha2\n");appears 30+ times across the rebase-op tests. Addassert_file_contents!(&test.repo, head_oid, "a.txt", "alpha2\n")intests/common.rsso call sites read declaratively and produce better failure messages including the file path. Build on T190 so the macro can callcommon::file_content_atdirectly. - T195 P2 feat - Build a
TuiTestHarnessto consolidate backend/terminal/draw/snapshot boilerplate: every TUI test repeats ~6 lines — createTestBackend, wrap inTerminal, callterminal.draw(|f| ...), clone the buffer, snapshot. Repeated 20+ times acrosstests/tui_*.rs. Addpub struct TuiTestHarnesstotests/common.rswithnew(width, height),render(|frame| { ... }) -> Buffer, and asnapshot()convenience that delegates toinsta::assert_debug_snapshot!. Reduces each TUI test to:let mut h = TuiTestHarness::std(); let buf = h.render(|f| views::commit_list::render(&mut app, f)); h.snapshot();. - T196 P3 feat - Introduce terminal-dimension constants for tests:
TestBackend::new(80, 24)/(120, 20)/(80, 10)/(60, 10)/(80, 12)are scattered across 25+ TUI test sites (tests/tui_squash_select.rs,tui_move_select.rs,tui_main_view.rs,tui_commit_detail.rs,tui_theme.rs,tui_fragmap.rs). Define a small set of named constants intests/common.rs—TERMINAL_STD: (u16, u16) = (80, 24),TERMINAL_WIDE: (u16, u16) = (120, 20),TERMINAL_SHORT: (u16, u16) = (80, 10),TERMINAL_NARROW: (u16, u16) = (60, 10),TERMINAL_PICKER: (u16, u16) = (80, 12)— and replace the magic numbers. Pairs naturally with T195'sTuiTestHarness::std()/wide()/short()constructors. - T197 P3 feat - Generalize the 3-commit TUI fixture into
common::create_n_commit_app(&[...]): the helpermake_app_in_squash_select/make_app_in_move_selectand similar in 6+ TUI test files all build anAppStatewhosecommitsfield is a hand-rolledvec![common::create_test_commit("aaa111…", "Oldest"), ...]. Addpub fn create_n_commit_app(summaries: &[&str]) -> AppStatetotests/common.rsthat synthesises deterministic OIDs from the index and populatescommits. Per-file helpers shrink to one or two lines and adding a 4th/5th commit to a test no longer requires inventing a fake OID. - T198 P3 feat - Add
common::create_drop_conflict(&TestRepo) -> ConflictStatefixture: the same 3-commit setup that triggers a drop conflict (base → adds line → depends on dropped line) appears attests/mergetool.rs:119and a couple of places intests/drop_commit.rs(e.g. lines 185–210). Extract a helper that returns the resultingConflictStateso tests focused on conflict resolution start with a one-line setup and read more like specifications. - T199 P3 feat - Centralize stub
GitRepovariants (NoOpRepo + FakeDiffRepo- a builder) in
tests/common.rs: TUI tests needGitRepoinstances that either panic on every call (NoOpRepo, see T191) or return a cannedCommitDifffor one method (FakeDiffRepolives inline intests/tui_commit_detail.rs). Once T191 lands, also liftFakeDiffRepoand add a small builder pattern (e.g.StubRepoBuilder::new().with_commit_diff(diff) .build()) so future TUI tests that need to mock anotherGitRepomethod can do so without copying the giant impl block.
- a builder) in
- [-] T200 P2 feat - Introduce file-path constants for tests: hardcoded
"a.txt","b.txt","c.txt","x.txt","y.txt","z.txt","root.txt","unrelated.txt"appear 50+ times acrosstests/{split,squash,drop,move}_commit.rsandtests/mergetool.rs. Definepub const FILE_A: &str = "a.txt";(etc.) intests/common.rsand use them; makes test file usage grep-able and lets a future rename touch one place. Pairs naturally with T194'sassert_file_contents!macro. (Flags: WONT DO) - T201 P2 feat - Add
assert_file_contents_at_head!macro: the patternlet head_oid = test.repo.head().unwrap().target().unwrap(); assert_file_contents!(&test.repo, head_oid, path, expected);recurred 15+ times at pure-HEAD assertion sites. Addedassert_file_contents_at_head!($repo, $path, $expected)totests/common/assert.rs(delegates toassert_file_contents!) and migrated all pure-HEAD call sites indrop_commit,move_commit,split_commit, andsquash_commit. Sites where the rawgit2::Oidis also used forfind_commit,revwalk,merge_base, orassert_eqcomparisons are left usingassert_file_contents!directly. - [-] T202 P2 feat - Add
TestRepo::file_at_head(path)shorthand: the pattern of looking up HEAD and reading a file's tree contents appears 50+ times after T190 lands aslet head_oid = ...; assert_eq!(common::file_content_at(&test.repo, head_oid, "a.txt"), ...). Addpub fn file_at_head(&self, path: &str) -> StringonTestReposo call sites becomeassert_eq!(test.file_at_head("a.txt"), "alpha2\n"). Halves the noise of HEAD lookups in assertions. (Flags: WONT DO) - [-] T203 P2 feat - Add
TestRepo::commits(&[(path, content, msg), ...])bulk-creation helper: the 3-commit setuplet base = test.commit_file(...); let mid = test.commit_file(...); let head = test.commit_file(...);recurs 20+ times acrosstests/{split,squash,drop,move}_commit.rs. Addpub fn commits(&self, configs: &[(&str, &str, &str)]) -> Vec<git2::Oid>onTestReposo tests can writelet [base, mid, head]: [git2::Oid; 3] = test.commits(&[(...), (...), (...)]).try_into().unwrap();(or destructure however ergonomic). Reduces ~80 LOC of noisy commit setup. (Flags: WONT DO) - [-] T204 P2 feat - Add
oid()/TestRepo::oid_of()conversion helpers: the conversion&Oid::from(commit_oid)(wherecommit_oid: git2::Oid) appears 30+ times across the rebase-op and mergetool tests, often clustered in the same call expression (e.g.git_repo.drop_commit(&Oid::from(to_drop), &Oid::from(head))). Add either a freepub fn oid(v: git2::Oid) -> Oidintests/common.rsor aTestRepo::oid_of(git2::Oid) -> Oidmethod so call sites simplify to.drop_commit(&oid(to_drop), &oid(head)). Trivial wrapper but removes a lot of visual repetition. (Flags: WONT DO) - T205 P3 feat - Move
create_fragmapandsimple_clusterhelpers intotests/common.rs:tests/tui_fragmap.rs:19-45definescreate_fragmap(...)and asimple_cluster(...)helper used 10+ times in that file, andtests/tui_squash_select.rs:255re-defines its own near-identicalsimple_cluster. Promote both topub fnintests/common.rs(parameterised over path / line range / commit OIDs) and import from both files; future TUI tests that need synthetic fragmap state get the helpers for free. - T206 P3 feat - Split large test files into sub-modules for navigability:
tests/split_commit.rs(1177 LOC),tests/squash_commit.rs(1046 LOC),tests/drop_commit.rs(737 LOC), andtests/move_commit.rs(476 LOC) currently use comment banners (// --- Conflict tests ---) to group related tests. Replace each with a thin entry-point that just declares sub-modules, e.g.tests/squash_commit.rsbecomesmod happy_path; mod conflict; mod dirty_state;with the actual tests intests/squash_commit/happy_path.rs,tests/squash_commit/conflict.rs, etc. Each sub-module declaresmod common;(or uses a shared path attr). Improves IDE file-tree navigation, surfaces the test taxonomy incargo testoutput, and creates natural homes for per-group fixtures. No logic changes. - T207 P3 feat - Add a
common::preludemodule re-exporting frequently used test imports: every rebase-op test starts with the same import block —use git_tailor::repo::{Git2Repo, GitRepo, RebaseOutcome}; use git_tailor::Oid; use anyhow::Result;plusmod common;. Addpub mod prelude { pub use crate::*; pub use git_tailor::repo::{Git2Repo, GitRepo, RebaseOutcome}; pub use git_tailor::Oid; }insidetests/common.rs(or astests/common/prelude.rs) so each test file can writeuse common::prelude::*;and drop ~5 lines of repeated imports. - T208 P2 feat - Add
TestRepo::write_file,stage_file, andcommithelpers and renamecommit_fileto reflect what it does:commit_file(path, content, message)actually writes the file to disk, stages it, and creates a commit — three distinct operations. (1) Addpub fn write_file(&self, path: &str, content: &str)that just writes the file to the workdir (replacing the repeatedlet workdir = test.repo.workdir().unwrap(); std::fs::write(...)pair at ~25 call sites acrossdrop_commit/dirty_state.rs,squash_commit/dirty_state.rs,split_commit/dirty_state.rs,move_commit/dirty_state.rs,reword_commit.rs, and others). (2) Addpub fn stage_file(&self, path: &str)that stages a single file (replacing the 4-lineindex.add_path+index.writeblock at ~10 call sites in the same files, plusdrop_commit/continue_abort.rs,drop_commit/error_cases.rs,split_commit/per_file.rs,commit_diff.rs). (3) Addpub fn commit(&self, message: &str) -> git2::Oidthat commits whatever is currently staged (useful incommit_diff.rswhere files are manually staged before committing, and as the building block forcommit_file). (4) Renamecommit_file→write_stage_commit(or a name the implementer prefers) so the name accurately describes the three-step operation; refactor its body to callwrite_file+stage_file+commit. Similarly refactorcommit_filesto delegate to the new primitives. No test-behavior changes — purely mechanical cleanup.
- T168 P2 bug - Commit detail view not shown when right panel is too narrow:
when the terminal is narrow or the separator has been moved far right,
entering commit detail mode ('i') keeps displaying the fragmap/chunk-group
matrix instead of the commit detail content; the app state correctly reflects
CommitDetailmode but the render path inmain_view.rscallscommit_detail::renderwith a very smallright_width— investigate whetherrender_in_area_without_fragmap_colsis painting over the right panel area, or whether theright_width > 0guard should have a higher minimum (e.g.MIN_RIGHT) before switching to the split layout, and fall back to full-screen commit detail when the right area is too narrow to show it usefully - T167 P3 feat - Show a persistent hint in the footer that
hopens help: append a short hint such asPress 'h' for key bindingsto the footer line rendered inrender_footerso first-time users can discover the help overlay without prior knowledge; the hint should appear in all modes that display the footer (commit list, commit detail) and be visually subordinate (e.g. dim style) so it does not compete with status messages or commit position info; when a status or error message is shown the hint should be suppressed so the two do not overlap
- T142 P3 feat - Support Ctrl-Z to suspend the TUI and return to the shell
(Unix only): in raw mode the kernel line discipline no longer converts Ctrl-Z
into SIGTSTP automatically, so the keystroke arrives as a key event; handle
KeyCode::Char('z') + CONTROLin the event loop by tearing down the TUI (disable raw mode, leave alternate screen — the same cleanup already done for the external editor/mergetool), then callinglibc::raise(libc::SIGTSTP)to suspend the process; when the user runsfgthe process receives SIGCONT, resumes afterraisereturns, and re-initializes raw mode and redraws; gate the entire feature on#[cfg(unix)]— on Windows the key event is silently ignored; the teardown/restore logic should be extracted into a shared helper to avoid duplication with editor.rs and mergetool.rs
- T116 P3 feat - Review codebase for refactoring opportunities: audit existing code for duplication, overly complex functions, inconsistent patterns, and areas where abstractions could simplify implementation; identify specific refactoring targets like extracting common dialog patterns, consolidating similar error handling, reducing parameter passing, and improving module boundaries; create follow-up tasks for the most impactful improvements
- T169 P1 feat - Extract shared list-selector key handling for squash_select
/ move_select / split_select: the three modal pickers in
src/views/{squash_select,move_select,split_select}.rseach implement near-identicalhandle_key(KeyCommand, &mut AppState) -> AppActionbodies (MoveUp/MoveDown/PageUp/PageDown with index clamping, Confirm, Quit, ShowHelp). Extract ahandle_list_navigation(action, cursor: &mut usize, len: usize, page_size: usize) -> ListNavhelper (or trait) in a newviews/list_nav.rs(or insideviews/dialog.rs) that returnsMoved,Confirmed,Canceled,Help, orUnhandled; each picker then becomes a small wrapper that mapsConfirmedto its mode-specificAppAction. Should remove ~100 LOC of near-duplication and make adding new pickers trivial. - T170 P1 feat - Reuse
build_conflict_stateacross drop / move / conflict-continuation paths:src/repo/git2_impl/squash_op.rsalready defines abuild_conflict_state(...)helper, butsrc/repo/git2_impl/drop_op.rs:65,src/repo/git2_impl/move_op.rs:82,src/repo/git2_impl/conflict.rs:41andsrc/repo/git2_impl/conflict.rs:87each constructRebaseOutcome::Conflict(Box::new(ConflictState { ... }))inline with duplicated field-population logic. Promotebuild_conflict_statetosrc/repo/git2_impl.rs(or a newrepo/git2_impl/conflict_builder.rs), generalize its parameters to cover all four call sites, and replace the inline constructions. Centralises conflict-state assembly so future fields (e.g. operation label for the conflict dialog header) only need to be added once. - T171 P1 feat - Consolidate
render_squash_footerandrender_move_footerinto a singlerender_action_footer:src/views/commit_list.rs:726andsrc/views/commit_list.rs:769are ~85% identical — both truncate the source-commit summary to the available width, build aLinewith key-hint spans (Enter / Esc), and apply the same dim/footer styling; only the action label and the instruction text differ. Replace both with a singlerender_action_footer(frame, app, area, label: &str, source_oid, instructions: &[(&str, &str)])helper and call it from both call sites (lines 684 and 693). Reduces ~40 LOC and ensures squash/move footers stay visually consistent. - T172 P1 feat - Split
dispatch_actionin main.rs into per-AppAction helper functions:src/main.rs:248definesdispatch_actionas a ~290-linematchoverAppActionwhere each arm contains 20–40 lines of side-effect logic (PrepareSplit, ExecuteSplit, PrepareReword, PrepareSquash, PrepareMove, …). Extract each non-trivial arm into a privatefn handle_<action>(...) -> Result<LoopAction>helper sodispatch_actionbecomes a thin dispatcher (~80 LOC) where each branch is one function call. Use the existingLoopAction/get_head_oid_or_continue!infrastructure; do not change behavior. Greatly improves navigability of the event loop and makes individual actions easier to reason about and test. - T173 P2 feat - Split
app.rsintoapp/state.rs+app/keymap.rs:src/app.rs(876 lines) currently mixes three concerns — theAppStatestruct and its many helper methods (move_, scroll_, page_*, set_message, …), theAppModestate-machine enum and its transitions, and theKeyCommandenum together withAppMode::parse_key/read_event. Convertapp.rsto a module declaration that ownsAppMode,AppAction, andSplitStrategy, moveAppStateand its inherent impls tosrc/app/state.rs, and moveKeyCommand,parse_key, andread_eventtosrc/app/keymap.rs. Re-export so external callers (main.rs,views/*) need no import changes. No behavior change. - T174 P2 fix - Replace hand-rolled scrollbars in commit_detail and dialog
with ratatui's built-in
Scrollbarwidget:src/views/commit_detail.rscontains two customParagraph-based implementations —render_scrollbar(vertical, ~45 LOC) andrender_h_scrollbar(horizontal, ~40 LOC) — that manually build"█"/"│"/"─"character strings;src/views/dialog.rshas a third,render_dialog_scrollbar(~35 LOC), with the same approach.commit_list.rsandhunk_groups.rsalready useratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState}correctly. Replace the three custom implementations with the same ratatui widget (usingVerticalLeft,VerticalRight, orHorizontalBottomas appropriate); the two-pass layout geometry incommit_detail.rsthat determines scrollbar area sizes must be kept — only the rendering step changes. Removes ~120 LOC of duplicated thumb-sizing arithmetic and aligns all scrollbars on a single rendering path. - T175 P2 feat - Extract cherry-pick helpers from
repo/git2_impl.rsintorepo/git2_impl/cherry_pick.rs:src/repo/git2_impl.rs(517 lines) currently houses the trait impl pluscherry_pick_chain(line 433),rebase_descendants(line 333),collect_descendants(line 402), and the internalCherryPickResulttype (line 510). These are the shared rebase primitives consumed by drop / move / squash / split ops and form a cohesive sub-module of their own. Move them (plus any required helpers) to a newsrc/repo/git2_impl/cherry_pick.rs, expose them throughpub(super)items, and re-export fromgit2_impl.rs. Bringsgit2_impl.rscloser to its trait-impl role and improves the mental model around rebase orchestration. - T176 P2 feat - Introduce a
Dialogbuilder to reduce dialog boilerplate: AddedDialogstruct tosrc/views/dialog.rswith a fluent builder API:blank(),title(),section(),styled_line(),plain(),wrapped(),wrapped_indent(),wrapped_styled(),wrapped_styled_bold(),key_binding(),instructions(),push_line(), andrender().title()adds surrounding blank lines implicitly;section()adds only a leading blank;render()pads the border title with spaces automatically. Refactoreddrop.rs,conflict.rs,help.rs, andsplit_select.rsto use it, removing ~55 net lines of repetitive span/style construction. - T177 P3 feat - Move domain types from
lib.rsinto adomain/submodule tree:src/lib.rscurrently mixes the public domain types (CommitInfo,FileDiff,Hunk,DiffLine,CommitDiff,DeltaStatus,DiffLineKind,Oid,VirtualOid) with the module declarations and re-exports. Split intosrc/domain/commit.rs(commit + oid types) andsrc/domain/diff.rs(diff/hunk/line types), then re-export fromlib.rsso external imports remain unchanged. Keepslib.rsfocused on crate-level wiring. - [-] T178 P3 feat - Extract
validate_operation_preconditionsfor drop / move ops:src/repo/git2_impl/drop_op.rsandsrc/repo/git2_impl/move_op.rsboth open with the same prelude — callcheck_no_dirty_state, parse the commit and head OIDs, look up the commit objects, validate parent count (single-parent only). Extract afn validate_single_parent_op(repo, commit_oid, head_oid) -> Result<(Commit, Commit)>helper ingit2_impl.rs. Saves ~10 LOC and removes a class of copy-paste hazards. (Flags: WONT DO) - [-] T179 P3 feat - Extract list-view scroll/selection helpers shared by
commit_list and commit_detail:
src/views/commit_list.rs(832 lines) andsrc/views/commit_detail.rs(822 lines) both implement scroll- bound clamping, page-size-derived navigation, and selection / scroll-offset coupling. Introduce a smallviews/list_view.rswithcompute_scroll_bounds(content_height, visible_height) -> (max_scroll, clamped_offset)and aListRenderContext { selection_idx, scroll_offset, visible_height }helper used by both; sets the pattern for any future scrolling list view. (Flags: WONT DO) - T180 P2 feat - Extract
compute_page_sizehelper in app.rs: the idiomvisible_height.saturating_sub(1).max(1)(keep at least one line of overlap when paging) is repeated atsrc/app.rs:485,:494,:501,:507,:737,:743(the dialog variant usesdialog_visible_heightbut the same arithmetic). Extract a small free functionfn page_size(visible_height: usize) -> usize(with a doc comment explaining the one-line overlap rule) and call it from all six sites; remove the inline// Keep at least one line overlapcomments now that the name documents the intent. - [-] T181 P2 feat - Extract scroll-offset clamping helper: the pattern
.min(max_scroll)for keeping a scroll offset within bounds appears insrc/app.rsand several places insrc/views/commit_detail.rs(around lines 179, 295, 431, 432) and dialog scroll handling. Add afn clamp_scroll(offset: usize, max: usize) -> usizehelper (orAppState::clamp_*_scrollmethods that wrap the field accesses) and use at all clamping sites. Reduces the chance of forgetting the clamp on a new code path. (Flags: WONT DO —.min(max)is already idiomatic; a wrapper adds no semantic value unlike page_size() which encodes a non-obvious rule) - T182 P2 feat - Add
VirtualOid::expect_real_oid()(orreal_oid_cloned) to eliminate.as_oid().unwrap().clone()chains: the patterncommit.oid.as_oid().unwrap().clone()appears atsrc/views/commit_list.rs:100,:112,src/views/squash_select.rs:92,:93, andsrc/views/move_select.rs:108. Add a method onVirtualOidsuch aspub fn expect_real_oid(&self, ctx: &str) -> Oidthat clones the innerOidor panics with a clear message if the variant is synthetic. Replace all five call sites; provides a single, well-named audit point if synthetic-vs-real handling ever needs revisiting. - T183 P3 feat - Replace
forward: boolparameter with aSearchDirectionenum:advance_search_match(app: &mut AppState, forward: bool)atsrc/views/commit_detail.rs:153is called with rawtrue/false, losing meaning at the call site. Defineenum SearchDirection { Next, Prev }(inapp.rsorviews/commit_detail.rs) and use it instead so call sites readadvance_search_match(app, SearchDirection::Next). Trivial change but improves grep-ability and readability. - T184 P3 feat - Extract
next_match_indexpure helper for search cycling: the wrap-around modulo arithmetic inadvance_search_matchatsrc/views/commit_detail.rs:157-166mixes cursor cycling logic withAppStatemutation. Extractfn next_match_index(current: Option<usize>, len: usize, dir: SearchDirection) -> usize(combine with T183) as a pure function so the cycling logic can be unit tested independently from the AppState plumbing. - T185 P3 feat - Extract
diff_path_with_prefixhelper for diff file headers:src/views/commit_detail.rs:569-575repeatspath.map(|s| format!("X/{}", s)).unwrap_or_else(|| "/dev/null".to_string())for botha/andb/prefixes when rendering the--- a/foo.rs/+++ b/foo.rsdiff header lines. Extractfn diff_path_with_prefix(path: Option<&str>, prefix: &str) -> Stringand call it twice; also defines a single place to change the/dev/nullsentinel if needed. - T186 P3 feat - Introduce
DIALOG_BORDER_HEIGHT/DIALOG_BORDER_WIDTHconstants inviews/dialog.rs: hardcodedsaturating_sub(2)/+ 2arithmetic representing the top+bottom (or left+right) border occupies dialog inner-area calculations atsrc/views/dialog.rs:45,:46,:80. Defineconst DIALOG_BORDER_HEIGHT: u16 = 2;(and width if applicable) at module top and replace the magic2s. Also a good template for future per-view layout constants. - T187 P3 feat - Replace
"staged"/"unstaged"string literals inVirtualOidwith named constants: the labels appear atsrc/lib.rs:88and:98(and in doc comments at lines 72-74) forVirtualOid::Staged/VirtualOid::Unstagedrendering. Defineconst STAGED_LABEL: &str = "staged";andconst UNSTAGED_LABEL: &str = "unstaged";at the top of the relevant impl block (or near theVirtualOiddefinition) and reference them from both arms, ensuring the two methods cannot drift out of sync. - T188 P3 feat - Introduce
ORIGIN_HEAD_REFconstant inrepo/git2_impl/reads.rs: the magic string"refs/remotes/origin/HEAD"is hardcoded insidefind_reference("refs/remotes/origin/HEAD")atsrc/repo/git2_impl/reads.rs:209, while related doc comments insrc/repo.rs:362-369andsrc/cli.rs:32-33reference the same ref shape. Addconst ORIGIN_HEAD_REF: &str = "refs/remotes/origin/HEAD";at the top ofreads.rsand use it; if the value ever needs to change (e.g. for a non-originremote default), there is one place to update. - T189 P3 feat - Switch
AppStateto#[derive(Default)]: the hand-writtenimpl Default for AppStateatsrc/app.rs:375-410enumerates ~25 fields, almost all of which already have natural zero/empty defaults. The only obstacle isreference_oid: Oid::from("")— addimpl Default for Oid(returning the empty-string variant with the existing semantics) soAppStatecan be derived. Reduces ~30 lines of mechanical boilerplate and means new fields withDefaulttypes no longer require touching the constructor. - T191 P2 feat - Replace
is_fixup: boolparameter with aSquashModeenum: the boolean is threaded throughAppMode::SquashSelect,AppAction,handle_prepare_squash, andenter_squash_or_fixup_select. Definepub enum SquashMode { Squash, Fixup }with methodslabel() -> &strandkeeps_target_message() -> bool, then replace allis_fixupparameters. Improves type safety and makes call sites self-documenting. - T192 P2 feat - Extract synthetic-commit guard helper: the pattern
if commit.oid.is_synthetic() { app.set_error_message("Cannot X ..."); return ... }appears 10+ times acrosscommit_list.rs,app/state.rs,move_select.rs. AddAppState::guard_real_commit(&mut self, action: &str) -> Option<&CommitInfo>that returnsNone(with error message set) when the selected commit is synthetic, replacing the boilerplate at each call site. - T193 P3 feat - Consolidate dialog enter/cancel helpers:
enter_split_confirm,enter_drop_confirm,cancel_split_confirm,cancel_drop_confirm,cancel_squash_select,cancel_move_selectinapp/state.rsall follow the same pattern (set mode + resetdialog_scroll_offseton enter, set mode toCommitListon cancel). Extract privateenter_dialog(mode)andexit_dialog()helpers and delegate from all 6+ methods. - [-] T194 P3 feat - Extract squash message preparation into a pure function:
handle_prepare_squashinsrc/main.rsis ~50 lines with 9 parameters (#[allow(clippy::too_many_arguments)]). Extract the message-construction logic (fixup vs squash, editor invocation decision) into a testable pure functionfn build_squash_message(is_fixup, source_msg, target_msg) -> String. (Flags: WONT DO — after T191 the message logic is just twokeeps_target_message()one-liners; not worth extracting) - T195 P3 feat - Split
compute_layoutincommit_list.rs: the function is ~83 lines computing fragmap dimensions, title widths, and layout areas. Break into 2-3 sub-functions (compute_fragmap_dimensions,split_table_areas) each handling one concern, with the main function as orchestrator. - T196 P3 feat - Restrict unnecessary
pubvisibility incommit_list.rs:build_header,build_constraints,fragmap_indexand similar helper functions are markedpubbut only used within the module. Removepubto narrow their visibility. - T197 P3 feat - Replace magic
2literals incommit_list.rslayout calculations with a named constant: the value represents the two column-gap characters (separator after SHA + separator before fragmap) and appears ~6 times incompute_column_widthsandcompute_layout. Defineconst COL_GAPS: u16 = 2;alongside the existingSHA_COL_WIDTH/MIN_TITLE_WIDTHconstants and replace all occurrences.
- T211 P2 feat - Start the TUI immediately and stream commits one-by-one
with a live counter dialog: added
commit_walkertoGitReporeturning a boxed iterator soGit2Repoyields one commit at a time from the underlyinggit2::Revwalk; addedAppMode::Loading { title, message, count }rendered by a newviews::loadingmodule as a centerd dialog overlay; the loading loop in the newsrc/loader.rsmodule renders at ~60 fps and polls for Ctrl-C withcrossterm::event::poll(Duration::ZERO)between commits — no background thread needed; splitload_with_progressinto three private helpers:walk_commits(iterator loop),confirm_matrix_build(Y/N dialog for large repos),build_hunk_group_matrix(fragmap computation with progress title); loading dialog shows"Loading Commits"title during the walk and"Hunk Group Matrix"during matrix computation; dialog border color changed fromDarkGraytoCyanto match other info dialogs; Y/N matrix confirm labels changed fromCompute/SkiptoYes/No. - T213 P2 fix - Replace
FragMapBuilderstep loop with a single-callbackbuild_fragmap()to fix unresponsiveness when one file's SPG takes too long: removeFragMapBuilderand itsstep()/run_dedup()/finish_matrix()methods; add aFragMapProgressenum with variantsClusteringFile { files_done: usize, files_total: usize },Deduplicating, andBuildingMatrix; changebuild_fragmapsignature tobuild_fragmap(commit_diffs: &[CommitDiff], deduplicate: bool, progress: &mut impl FnMut(FragMapProgress) -> bool) -> Option<FragMap>where the callback returnstrueto continue andfalseto interrupt (returningNonefrombuild_fragmap); thread the callback down throughbuild_file_clusters→build_file_clusters_and_assign_hunks→build_file_spg(inspg.rs), calling it after each commit generation is processed insidebuild_file_spg's main loop to ensure responsiveness even for a single large file; also call it at the outer file-loop boundary (updatingfiles_done), before deduplication, and before matrix construction; updatebuild_hunk_group_matrixinloader.rsto callbuild_fragmapwith a closure that renders the loading view, polls crossterm fors/S(skip), and updatesapp.modewith the appropriateAppMode::Loadingvariant for each phase — the closure capturesterminal_guardandappby mutable reference;build_hunk_group_matrixstaysResult<Option<FragMap>>(theResultwraps terminal I/O errors from rendering);build_fragmapitself staysOption<FragMap>with noResultsince it has no I/O; updateassign_hunk_groups(used by split) to keep its current internal structure but accept an optional no-op progress callback if needed for consistency; add or update any tests that directly usedFragMapBuilder.
- T212 P3 feat - Introduce semantic dialog kinds and text roles to eliminate
scattered
Colorliterals from dialog call sites: add aDialogKindenum (Info,Confirm,Danger) whose variants map to a fixed border color (Cyan,Yellow,Redrespectively — matching the existing conventions); changeDialog::renderto acceptDialogKindinstead of a rawColorfor the border; add aTextRoleenum (Normal,Highlight,Muted,Key,Danger) and correspondingDialogbuilder methods (role_line,role_wrapped, etc.) that resolve the role to aColorinternally; update all call sites inviews/(drop.rs,conflict.rs,split_select.rs,help.rs,loading.rs,squash_select.rs,move_select.rs) to use the new API; thetheme.rsmodule (or a newdialog_theme.rssibling) owns theDialogKind → ColorandTextRole → Colormappings so a future theme switch only needs to touch one place.
- T190 P2 feat - Support dropping the root commit: currently
drop_commitbails with "Cannot drop a merge or root commit" whencommit.parent_count()== 0; updatedrop_op.rsto handle the root case separately — collect all descendants, make the first descendant an orphan root commit (using its existing tree and metadata, reusing theplan_move_root_to_laterpattern frommove_op.rs), then cherry-pick the rest of the chain on top; split the parent-count guard into two branches:parent_count > 1bails with "Cannot drop a merge commit",parent_count == 0takes the root path,parent_count == 1is the existing fast path; also updatevalidate_single_parent_op(or introduce a separatevalidate_non_merge_op) if the refactored helper from T178 makes the split guard awkward; add a test intests/drop_commit/root_commit.rsthat verifies the root commit is dropped and the history is correctly rewritten. - T214 P2 feat - Allow squash/fixup into the root commit: currently
squash_commits(and fixup) bail when the target commit has no parent because the cherry-pick chain requires a base tree; handle the root case by squashing the source commit's diff directly onto the root's tree, then creating a new root commit (no parents) with the combined tree and message; the source commit should then be removed from the chain using the existing rebase logic; add tests intests/squash_commit/covering squash-into-root and fixup-into-root. - T215 P1 bug - Fix spurious conflict when squashing across a rename: when
squashing commit B into an earlier commit A where a file touched by both was
renamed in a commit between them, the tool incorrectly reports a conflict and
leaves both the old and new filename to resolve — even though
git rebase -icompletes cleanly; investigate howsquash_op.rsbuilds the cherry-pick chain across renames (the intermediate rename commit changes the path, so the cherry-pick of A's diff onto the post-rename tree likely applies to the wrong path); compare with howmove_op.rshandles rename tracking; the fix should make the squash cherry-pick chain path-aware — either by detecting the rename and rewriting the diff path before applying, or by using the post-rename path consistently throughout the chain; add a regression test intests/squash_commit/with a rename between the squash source and target. - T217 P1 bug - Fix wrong highlight row in hunk group matrix during move
commit (
m): when the move-select dialog is open, the highlighted row in the fragmap / hunk group matrix is always two rows below the empty placeholder line that marks the insertion point; investigate howmove_select.rs(ormain_view.rs) computes the highlighted matrix row frominsert_beforeand trace back to where the off-by-two offset originates; fix the index calculation so the highlighted row tracks the insertion-point placeholder exactly; add or update thetui_move_selectsnapshot tests to cover the highlighted-row position. - T218 P2 feat - Add a "split out file" split option for multi-file commits:
extend the split strategy menu with an additional option that applies when a
commit touches multiple files and allows the user to peel one file's changes
out into its own commit while keeping the remaining file changes together in
the original commit's replacement; selecting this option from the split menu
should open a second dialog listing the changed files in the selected commit,
let the user choose which file to split out, then execute the rewrite as a
two-commit split (chosen file first or otherwise consistently ordered);
update the split TUI state/mode flow, add the backend split operation and
validation/counting logic, and cover the new menu/dialog flow with TUI tests
plus repository tests in
tests/split_commit/. (Note: T218 is a known duplicate task number — see the other T218 below, "Add undo/redo of history-rewriting operations". Both were merged long ago under this number; not worth renumbering now.)
- T143 P3 feat - Add half-page scrolling to the commit detail view: bind
Ctrl-D/Ctrl-U(vim convention) andCtrl-PageDown/Ctrl-PageUpto scroll approximately half the visible content area at a time; the scroll amount should be derived from the current panel height so it stays proportional regardless of terminal size - T144 P3 feat - Add jump-to-top/bottom keybindings in the commit detail
view: bind
g/G(less/vi convention) andHome/Endto scroll to the very first or very last line of the diff content - T145 P3 feat - Add horizontal scroll-to-edge keybindings in the commit
detail view: bind
0/$(vi/less convention),Ctrl-A/Ctrl-E(emacs convention), andCtrl-Home/Ctrl-Endto scroll the diff content fully left (column 0) or fully right (rightmost position) respectively - T146 P3 feat - Make the help overlay context-sensitive: pressing
?(orh) in the commit detail view should show only the keybindings relevant to that view (scrolling, search, navigation back), while pressing it in the commit list shows only commit-list bindings; the current single monolithic help window is becoming too long as new keybindings are added; implement by passing the currentAppModeto the help renderer and selecting the appropriate subset of bindings to display - T165 P3 feat - Navigate between files in commit detail view by pressing
f: pressingfshould jump the scroll position to the start of the next file's diff block in the commit detail view; pressingF(shift) should jump to the previous file; the file boundary can be detected from the rendered line list (eachFileDiffentry starts with a file header line); wrap around when reaching the end/beginning of the file list so the navigation is cyclic - T209 P2 feat - Add
Space/b(less convention) andCtrl-F/Ctrl-B(vi convention) page-scroll keybindings in the commit detail view:SpaceandCtrl-Fscroll one page down,bandCtrl-Bscroll one page up; the scroll amount should match the existingPageDown/PageUpbehavior (one visible-area height, keeping one line of overlap)
- T216 P2 feat - Add a persistent operation journal for crash safety: the
cherry-pick rebase operations (move, drop, squash, fixup, reword, split) hold
their in-flight state only in memory — in particular
ConflictState(original_branch_oid,new_tip_oid,remaining_oids, the conflicting commit and files, etc.) lives inAppStatewhile the user resolves a conflict. By that point the branch ref has already been advanced to a partial tip and the working tree holds conflict markers, so if gt is killed mid-operation the remaining-work state is lost: the operation cannot be resumed and the repo is left mid-conflict. Persist operation state to a durable journal under.git/(e.g..git/git-tailor/journalfor the serializedConflictState, plus a ref such asrefs/git-tailor/origrecording the pre-operation tip so the original commits are pinned againstgit gc). Write/refresh the journal when a mutating operation starts and when it enters a conflict; clear it on successful completion or abort. On startup, detect a leftover journal entry (an interrupted operation) and offer the user a recovery dialog: resume the rebase from the persistedConflictState/remaining_oids, or abort by restoring the branch ref to the recorded original tip and cleaning the working tree. Keep this git2-native — do NOT write or depend on git's private.git/rebase-merge/format, sogit rebase --continue/--abortwill not act on this journal (recovery is via gt); the reflog remains a manual fallback (git reset --hard <branch>@{1}). Add integration tests that build aConflictState, persist the journal, drop and reopen the repo handle, and assert the interrupted operation is detected and that both resume and abort restore correct state. NOTE: replacing the cherry-pick engine withgit2::Rebasewas investigated and rejected — libgit2 only exposes the non-interactive, range-based rebase (git_rebase_initoverupstream..branch) and cannot express git-tailor's reordering operations (move, non-adjacent squash), which require an arbitrary commit order; its in-memory mode also writes no on-disk recovery state. A native journal delivers the crash-safety goal for all operations and is the shared foundation for undo (T218). - T218 P2 feat - Add undo/redo of history-rewriting operations via an
operation stack: because every gt mutation (move, drop, squash, fixup, reword,
split) only builds new commits and advances the branch ref — the previous
commits remain in the object database — undo needs no per-operation inverse; it
simply restores the branch ref to the tip OID recorded before the operation and
checks out. Maintain a stack of operation records
{ label, tip_before, tip_after }persisted alongside the T216 journal; undo pops the top record and restorestip_before, redo restorestip_afterand pushes it back, with multiple levels supported by walking the stack. Pin the recorded tips againstgit gcby writing refs underrefs/git-tailor/undo/<n>(a plain file holding a SHA does not protect objects from gc — only refs/reflogs do). Bind undo and redo to free keys in the commit-list view (uis taken by reload andrby reword, so choose unused keys) and document them in the help dialog. Safety: run the same dirty-state guard the operations use before undoing (a hard reset would clobber uncommitted changes), and validate that HEAD still matches the expectedtip_afterbefore allowing undo — if the user rewrote history via external git the stack is stale and must be invalidated or trimmed. Add integration tests: perform each operation, undo and assert history/file contents match the pre-operation state, redo and assert they match the post-operation state, plus multi-level undo/redo and stale-stack invalidation. Depends on T216 (journal infrastructure). (Note: T218 is a known duplicate task number — see the other T218 above, "Add a 'split out file' split option". Both were merged long ago under this number; not worth renumbering now.) - T219 P2 feat - Add opt-in auto-stash so dirty-working-tree operations just
work: operations that currently refuse when the working tree has staged or
unstaged changes (
move,drop,squash,fixupviacheck_no_dirty_state, andundo/redo, which hard-reset the tree) should, when auto-stash is enabled, automatically stash the dirty state, run the operation, then restore it afterwards instead of bailing. Gate it behind a new CLI flag--autostashwith aGT_AUTOSTASHenv binding (default off, mirroring--reverse/GT_REVERSE), matching git's ownrebase.autoStashergonomics. Requirements:- Preserve the staged/unstaged split exactly: changes staged before the
operation must be staged again afterwards, and unstaged changes must come
back unstaged. (git2 supports this via
stash_savethenstash_apply/stash_popwithREINSTATE_INDEX; alternatively unstage the index and take a second stash so the two sets restore independently.) Include untracked files so nothing is lost. - Conflict-bearing operations: when the operation enters
RebaseConflictthe working tree holds conflict markers and the stash cannot be popped yet — defer the unstash until the operation truly finishes (afterrebase_continuecompletes) or is aborted (rebase_abort), restoring the original staged/unstaged state in both cases. Surface a clear error if the stash cannot be reapplied cleanly (it conflicts with the rebased result) rather than silently dropping it. - Crash safety: record the stash reference in the operation journal (T216) so that if gt is killed between stashing and restoring, the recovery flow can reapply (or at least point the user at) the stash instead of leaving work stranded in the stash list.
- Undo/redo (T218):
undo/redoreset the working tree, so with auto-stash on they must stash before and restore after, the same as forward operations, keeping the user's in-progress edits intact across an undo/redo. The dirty-state guard inapply_undo/apply_redoshould defer to the auto-stash path when enabled. - Plumb the flag from
cli.rsintoAppState/ the repo layer and thread it to every guarded operation; when disabled, behavior is unchanged (still refuse with the current message). Add integration tests covering: staged-only, unstaged-only, and mixed dirty state restored exactly after move/squash; the conflict path (stash reapplied after continue and after abort); untracked files preserved; and an undo-with-dirty-tree round trip. Depends on T216 (journal) and interacts with T218 (undo/redo).
- Preserve the staged/unstaged split exactly: changes staged before the
operation must be staged again afterwards, and unstaged changes must come
back unstaged. (git2 supports this via
- T220 P2 feat - Stage all unstaged changes from within git-tailor: add a
key binding in the commit list (e.g.
afor "add", currently unused) that stages every unstaged working-tree change — modifications, additions (untracked files), and deletions — equivalent togit add -A. Add astage_allmethod to theGitRepotrait (git2:Index::add_all(["*"], …)plusupdate_allto capture deletions, thenIndex::write) and wire the key throughcommit_list::handle_keyand a newAppAction, reloading afterwards so the synthetic "staged" / "unstaged" rows refresh. Show a status message, including a no-op message when there is nothing to stage. Document the key in the help dialog. Scope: staging all changes at once is enough for now — per-file or per-hunk staging is out of scope. - T221 P2 feat - Commit staged changes from within git-tailor: add a key
binding in the commit list (e.g.
cfor "commit", currently unused) that creates a new commit from the currently staged changes. Open the configured editor (reuseedit_message_in_editor) for the commit message; if the message is non-empty, build a tree from the index and create a commit with the current HEAD as parent, advancing the branch ref (cancel on an empty message, as reword does). Add acommit_staged(message)method to theGitRepotrait, and refuse with a clear message when nothing is staged. Reload afterwards so the new commit appears and the "staged" synthetic row clears; document the key in help. Scope: committing all staged changes with an editor-provided message is enough for now. Decide how this interacts with undo/redo (T218): a plain commit is additive rather than history-rewriting, so it need not be undoable in this task — but record the decision rather than leaving it implicit.
- T223 P3 feat - Add a
--clean-journalCLI option that wipes all git-tailor recovery state: delete the journal file (<gitdir>/git-tailor/journal.json, and thegit-tailordir if it ends up empty) and every ref git-tailor writes underrefs/git-tailor/*— the undo pins (refs/git-tailor/undo/*) and the in-progress pin (refs/git-tailor/orig) — discovering refs by globbingrefs/git-tailor/*rather than from the journal contents, so stray refs are removed even if the journal is missing, corrupt, or out of sync. This is a manual escape hatch for when recovery state gets stuck. The option must NOT start the TUI: it performs the cleanup and exits (like the static-output path), and is meant to run on its own — combining it with the normal browse arguments should be rejected with a clear error (or those args ignored). Write a short summary to stdout when finished (e.g. whether a journal file was removed and how many refs were deleted). Implementation: add the flag incli.rs; branch early inmain.rsbefore terminal setup; enumerate-and-delete the refs viareferences_glob(best-effort, continue past individual failures) and remove the journal file, reusing/extending thejournalmodule rather than duplicating ref names. Add integration tests that seed a journal file plus undo/orig refs (including a strayrefs/git-tailor/undo/*not referenced by the journal), run the cleanup, and assert the file and all refs are gone and the summary reports them.
- T225 P3 feat - Scroll the commit list with
Ctrl-Up/Ctrl-Downwithout moving the selection: bindCtrl-Up/Ctrl-Down(currently unused —Ctrl-Left/Rightadjust the separator andCtrl-PageUp/Downhalf-page scroll) to scroll the list viewport by one row while keeping the selected commit highlighted, like vim'sCtrl-Y/Ctrl-E. Only scroll as far as the selection stays visible — the selected row must never leave the visible window. Today the scroll offset always follows the selection, so this needs an independent scroll offset clamped againstcommit_list_visible_height(and the fragmap/detail layout). Make it behave intuitively in reverse-order mode (--reverse) too, and document the keys in the help dialog.
- T224 P3 feat - Show diff context around staged/unstaged changes in the
commit detail view: the synthetic Staged/Unstaged rows render their diff with
no surrounding context, while real commits show the default context, so the
detail view is inconsistent.
reads::staged_diff/unstaged_diffsetcontext_lines(0)(needed for tight fragmap span extraction), and the detail view reuses that same diff. Show the same amount of context as a commit diff (commit_diff, default 3) for the detail view while keeping the 0-context spans for the fragmap — e.g. thread a context-lines parameter through the synthetic-diff reads, or add a detail-specific variant mirroring the existingcommit_diffvscommit_diff_for_fragmapsplit. Relates to T166 (adjustable context), which should then also apply to the staged/unstaged rows.
- T140 P3 feat - Add shell completion for CLI options: use
clap_completeto generate static completion scripts (bash, zsh, fish) for all flags and value_enum variants (e.g.--squashable-scope). NOTE: zero-setup completions require distribution via a package manager (apt, brew, etc.) that can deposit the script in the right system directory at install time; users installing viacargo installwill still need a manual one-time setup step. - T141 P3 feat - Add branch/tag completion for the BASE argument: extend the
completion mechanism from T140 so that the positional
baseargument offers branch and tag candidates by queryinggit2for local branches, remote-tracking refs, and tags; degrade gracefully if the current directory is not inside a git repository. Same distribution requirement as T140. - T210 P3 feat - Add
gt completionssubcommand to generate and install shell completion scripts:gt completions --shell <bash|zsh|fish>prints the generated script to stdout; adding--installwrites it to the conventional user-local path without requiring root — bash:~/.local/share/bash-completion/completions/gt, zsh:~/.local/share/zsh/site-functions/_gt, fish:~/.config/fish/completions/gt.fish; print a hint after install explaining any shell-reload step needed (e.g.source ~/.bashrc); this removes the manual setup burden forcargo installusers and makes T140/T141 completions self-contained without depending on a package manager - [-] T138 P3 feat - Add syntax highlighting to diff code in commit detail view:
use
syntect(already a transitive dependency) to highlight the code portions of diff hunks based on the file extension / language; convert syntect's(Style, &str)token pairs to ratatuiSpans with mapped foreground colors; diff-specific styling (green/red for added/removed lines, hunk headers) should remain and take precedence — syntax colors apply to the code content within those lines; add asyntect::parsing::SyntaxSetandsyntect::highlighting::ThemeSetto the application state (loaded once at startup) so highlighting is performed per-hunk on demand without re-loading assets; consider caching highlighted output per commit to avoid re-highlighting on every render. (Flags: WONT DO —syntectis not actually a dependency and is heavy to add; the current solid-fg +/- line coloring cannot coexist with per-token syntax colors without a delta-style background-tint redesign; and correct highlighting needs full old/new file blobs we do not store. See planinvestigate-task-t138.) - T166 P3 feat - Increase and decrease diff context lines in commit detail
view with
+and-: pressing+should increase the number of context lines shown around each hunk (default 3, matching git's default), and-should decrease it (minimum 0); store the context line count inAppStateand pass it through tocommit_diff(or re-render the cached diff with the new context); changing the value should trigger a re-fetch or re-render of the diff so the change is immediately visible; show the current context line count in the footer or status line so the user knows the active value - T226 P2 bug - Make the header/footer/separator chrome readable across
terminal themes.
HEADER_STYLE,FOOTER_STYLE,SEPARATOR_STYLE(incommit_list.rs) and the status bar inmain_view.rspaintfg Whiteon ANSI-indexed backgrounds (bg Green/bg Blue/bg Cyan), assuming those ANSI slots are dark enough for white text. On pastel themes that remap ANSI green/blue to light shades (e.g. Catppuccin Mocha: blue#89b4fa, green#a6e3a1) the white-on-light text washes out, as does theDarkGray"Press 'h' for help" hint on the footer. Make the chrome contrast-safe on any terminal palette — prefer self-consistent explicit RGB (or reverse-video) for the bars instead of inheriting ambiguous ANSI background slots, so it is readable by default. A separate opt-in flag for overall UI coloring (analogous to--theme, which today only styles the hunk-group matrix) could be a follow-up nicety but should not be the primary fix.
- T230 P2 refactor - Interface-segregate the
GitRepogod trait (54 methods,src/repo.rs). Split it into focused traits:RepoRead(the 17 read/query methods) plus mutation traits (SplitOps,SquashOps,RewriteOps= drop/move/reword/edit,RebaseOps,JournalOps,UndoOps,StagingOps,StashOps), keeping a bundletrait GitRepo: RepoRead + SplitOps + … {}with a blanket impl so existing&impl GitRepobounds keep compiling.Git2Repo's impl is already a thin delegation layer, so the impl regroups rather than changes. Then narrow the read-only consumers (loader.rs,views/commit_detail.rs,views/main_view.rs,editor.rs) to&impl RepoRead, and shrink the test doubles: today 74unimplemented!()stubs acrossStubRepo(tests/common/fake.rs, 49/54) andMockRepo(src/dispatch/tests.rs, 25/54) —StubRepobecomes aRepoRead-only stub. Orthogonal to T240 (the lowerGitBackendseam belowGitRepo, split out of T222); this segregates the surface above it. Pure refactor, behavior-preserving. - T231 P2 refactor - Factor repeated dispatch-handler scaffolding
(
src/dispatch/*). (a) Theautostash_save()-guard block is copied verbatim 8× (commit_ops.rs, split.rs, edit.rs, autofixup.rs) → one helper. (b) The "suspend TUI +$EDITORon a message + empty/unchanged match" appears 5× (commit_ops.rs commit-staged/reword/squash, conflict.rs squash-continue, autofixup.rs edit message) → a helper returning anEditedMessage { Text | Empty | Unchanged }. (c)handle_run_mergetool/handle_run_editor/handle_run_stash_tool(conflict.rs) are three near-identical "suspend → run tool → refresh conflicting-files → rebuild conflict-state → banner" flows (the stash one is already the mergeduse_mergetool: boolshape) → onerun_conflict_toolparameterized by the tool closure and target-state builder. (d) drop/move handlers are line-for-line identical but the git call + labels → a shared wrapper. Pure refactor; MockRepo dispatch tests already cover these paths. - T232 P2 refactor - Factor the
cherry_pick_chain"finish" wrappers (src/repo/git2_impl/*). The Complete/Conflict result match is inlined 6× (drop_op.rs:57, move_op.rs:79, cherry_pick.rs:258, squash_op.rs:318, conflict.rs:79, edit_op.rs:155); squash already extractedreplay_and_advance— generalize it toadvance_and_finish(repo, chain_result, checkout_target, log_msg)and route the other five through it. Also collapse the 3×ConflictStateconstruction (cherry_pick.rs:167/225, squash_op.rs:281) into one builder, and the 3×revwalk push→collect→reverseidiom (drop_op.rs:75, move_op.rs:101/155) and 4× empty-tree build into small helpers. Pure refactor; covered by existing integration tests. - T233 P3 refactor - Replace the
ConflictStatefat union with honest per-op state (src/repo.rs:103). It carries the common conflict fields plus four op-specific optional payloads (moved_commit_oid,squash_context,autofixup_context,edit_context) + anis_orphan_rootflag, with consumers branching on which isSome; it is also abused bybegin_edit(edit_op.rs) to journal an in-progress edit that has no conflict. Move toward an enum-of-contexts and separate the "in-progress journal record" from "conflict awaiting resolution". Touches journal serialization + crash recovery → do TDD againsttests/undo.rsand the edit/recovery tests. Higher risk. - T234 P3 refactor - Break up the
AppStategod-struct (src/app/state.rs, 34 flat fields). Extract the repeated(offset, max, visible_height)scroll state — detail vertical, detail horizontal, and every dialog — into a reusableScrollState, and group the detail-view, search and status fields into sub-structs. The commit-list fields are not a third scroll-triple: there is nomax(the bound comes fromcommits.len()), the offset is anOptionoverride, and the effective offset also needscommits/reverse/selection_index. Group those by cohesion instead — all five together in aCommitListStatethat owns the navigation, the scroll override and the row queries — so each becomes a real method rather than one reaching across four fields. The two row helpers that also set an error message keep their signatures onAppState, which composes list + status. Movepending_autofixup_selectionoffAppStateentirely (the one transient-per-op field that leaks into cross-cutting state). Separately, lift the self-contained ~10-function detail search subsystem out ofviews/commit_detail.rs(929 lines) into its own module. Pure refactor. - T235 P3 refactor - Unify the two descendant-replay engines.
reword_op.rsandsplit_op.rs(finalize_split) use their ownrebase_descendants(cherry_pick.rs:28), which duplicates the cherry-pick mechanics of the conflict-awarecherry_pick_chain(drop/move/squash/edit) and differs only in what it does with a conflict. Share the step, but keep the distinction: split and reword replay onto a commit whose tree is identical to the original's, so the merge takes theirs at every path and the result equals the descendant's own tree — inductively down the chain, a conflict is impossible. Give that path a return type with no conflict variant, so callers are never made to handle an impossible case, and have it bail without journaling, writing the working tree or moving a ref. Two preconditions: per-file split must pin its last piece to the original tree (the one strategy where that invariant is emergent rather than structural), and both operations must reject merge commits in the replay range, which make the descendant revwalk unreliable. Cover with tree-identity assertions — a descendant-conflict test is unconstructible. - [-] T236 P3 refactor - Split the two grab-bag files in the git2 layer
(
git2_impl/journal.rs,git2_impl/reads.rs) if they keep growing. WON'T DO — the trigger never fired and the "grab bag" premise is wrong. Both files are stable:reads.rshas been flat for two months (491 → 544 → 512 — it shrank), andjournal.rsgrew 200 → 811 lines in its first 11 days then only +48 in the five weeks since, the last +43 of that being T233 refactor churn rather than new responsibility.journal.rsis also not a grab bag but a single persisted document (JournalDoc, onejournal.json) with accessors: 11 of its 15pub(super)functions open withload_docand 10 close withsave; the supposed five concerns are four fields of that one struct;is_emptydeliberately couples their lifecycles (the file is deleted only when all are empty at once); and the gc-pins are not state but a pure function ofundo+redo, recomputed on every save. Splitting it would mean exposingJournalDocand all its fields plusload_doc/write_doc/save/UndoRecord— an encapsulated core turned into a module-wide API to make one file shorter. (The original inventory also missed the in-progress/crash-record group, the most externally called cluster at 14 sites.)reads.rsis 25 functions averaging 16 lines, cohesive by role and clustered around shared private helpers that a split would cut across module boundaries. Re-open only if either file gains a genuinely independent concern — one with its own lifecycle, not another field ofJournalDoc. Not on line count. - T237 P3 refactor - Reduce view-layer duplication. Five near-duplicate
scroll-into-view helpers (operation_select, split_select, split_files_select,
split_hunks_select, and autofixup — the last with variable-height items) →
one
ScrollState::ensure_visible(start, height). Thereverseup/down mirroring is duplicated across three modules (commit_list.rs handle_key with eight copies, list_nav.rs with four, move_select.rs folding it intoup ^ reverse) → mirror the key once viaKeyCommand::with_vertical_mirroring, so handlers reason in one direction and the display order is resolved in a single testable place.ScrollListUp/ScrollListDownmust stay unmirrored: they move the viewport in display space and are already visual. Also collapse move_select's four near-identical navigation arms into one, and single-source the paging math onapp::scroll::page_size. None of this had any test coverage — commit_list's handler had never been sent a navigation key — so land characterization tests first and require them to pass unchanged across every refactor.
- T227 P2 feat - Add a "split out hunk(s)" split option, mirroring T218's
"split out file" at hunk granularity: peel one or more selected hunks
(possibly across several files) out of a commit into their own commit while
the rest stay together in the original commit's replacement. Selected from
the split-strategy picker like every other strategy; since picking hunks
needs the user to see the code (a bare file+line-range label isn't enough),
confirming it opens a dedicated wide two-pane dialog
(
AppMode::SplitHunksSelect,src/views/split_hunks_select.rs) — a scrollable list of the commit's hunks (file path + old-side line range) on the left, a colored diff preview of the highlighted hunk on the right, mirroring how the main window splits the commit list from the detail view.↑/↓move the cursor,vtoggle-selects the hunk in view,Entersplits out the marked hunks (falling back to just the hunk under the cursor when nothing is explicitly marked),Esccancels. The backend operation (GitRepo::split_commit_out_hunks,src/repo/git2_impl/split_op.rs, reusing the existing hunk-application helpers inhunks.rs) identifies hunks as(delta_idx, hunk_idx)against the diff at a fixed context level (repo::DEFAULT_CONTEXT_LINES) — the same level the picker itself loads the commit's diff at, viaHunkPickerEntry(src/app.rs), keeping the two consistent without needing a separate zero-context diff. Executes as a two-commit split via the existing "two-tree trick" (split_commit_out_file's approach). Covered by repository tests intests/split_commit/out_hunks.rsand TUIhandle_key/snapshot tests intests/tui_split_hunks_select.rs.
- T228 P2 feat - Add an "Edit" operation (interactive-rebase's
editverb): pause on the selected commit with its tree checked out — as ifgit rebase -ihad stopped there — and drop the user into a shell to freely edit files,git add, andgit commit(including splitting into an arbitrary number of commits with custom boundaries, e.g. viagit add -p); when the shell exits, continue. Reusesrc/external_tool.rs::with_tui_suspended(today used for$EDITORand the mergetool) to suspend/restore the TUI, spawning$SHELL(falling back to a sensible default, e.g./bin/sh, if unset) instead; show an on-screen message before suspending explaining what to do and that exiting the shell continues. On resume, detect the resulting commit chain from the original parent to the new HEAD and splice it in place of the original commit, replaying descendants — reuse the exactfinalize_split/rebase_descendantsmachinerysplit_commit_per_*already uses insrc/repo/git2_impl/split_op.rs(Edit is architecturally a Split whose pieces are user-authored rather than computed). Needs a validation step before splicing — confirm the resulting HEAD still descends from the expected parent commit — and a clear, safe abort path if the user leaves the repo in an unexpected state (checked out elsewhere, a merge commit, etc.), in the spirit of the existing interrupted-operation journal/recovery system; a no-op (shell exited with no changes) should behave as a canceled operation, not a rewrite. Make the operation undo/redo-able like every other history-rewriting operation. Cover with repository tests (multi-commit output, no-op case, unexpected-state abort) and TUI tests for the suspend/resume flow.
- T229 P2 feat - Add bulk "Autofixup" (mirrors
git rebase --autosquash): a new action (not tied to a single selected commit) that scans the branch forfixup!/squash!-prefixed commits, matches each to the earlier commit whose summary line follows the prefix, and squashes/fixups each into its target in one bulk pass — bottom-up, respecting each target's position, so multiple fixups for the same target stack correctly. Reuse the existing squash/fixup backend (src/repo/git2_impl/squash_op.rs) as the primitive, looping it over the computed target pairing; show one confirmation dialog up front listing what will happen before running (the whole batch is a single undoable operation via the existing journal, like every other rewrite). Cover with repository tests (multiple fixups targeting the same commit, a fixup with no matching target, mixed fixup!/squash! prefixes) and a TUI test for the confirmation dialog.
- T238 P2 human - Watch the promo video end to end with fresh eyes and
tighten whatever grates (Flags: HUMAN TASK). Every scene has been checked
against its own narration and timings, but the whole thing has never been
judged as one piece by someone not holding the numbers in their head. Render
with
demo/build.sh video; the pacing levers and what each is worth are indemo/promo/README.md. - T239 P2 human - Publish the promo video and link it from
README.md(Flags: HUMAN TASK). Upload to YouTube, then link it as a clickable thumbnail — an image wrapped in a link. Do not embed<video>or an MP4: GitHub sanitises the tag out of rendered Markdown and crates.io ignores it, so an embed silently degrades to nothing on both. Done, minus the thumbnail: the video is published and the README's "## Videos" section links the playlist. The thumbnail requirement predates the decision to publish a playlist — a stable address, with individual videos replaced rather than updated when the interface changes — and was dropped rather than met. Not because a thumbnail would go stale: a committed local image wrapped in the playlist URL would not, only hotlinkingimg.youtube.com/vi/<id>/…would. It was dropped because the README already opens withdoc/demo.gif, and a static thumbnail a few lines below would compete with an animated demo doing the same job better.
- T118 P2 feat - Set up GitHub Releases with pre-built binaries: create
.github/workflows/release.ymlthat triggers on version tags (v*), builds thegtbinary forx86_64-unknown-linux-musl(fully static, covers WSL2 and all Linux distros),x86_64-pc-windows-msvc(Windows native), and optionallyaarch64-unknown-linux-gnuandaarch64-apple-darwin; usetaiki-e/upload-rust-binary-actionto strip, archive, and attach binaries to the GitHub Release automatically; the musl target should produce a zero shared-library binary (addRUSTFLAGS=-C target-feature=+crt-staticif needed) so no system libs beyond the kernel are required