Skip to content

Conversation

@alfatm
Copy link

@alfatm alfatm commented Dec 18, 2025

Hey Kyosuke, Hope you're doing well! Here's what this PR brings to Serie:

Motivation

Serie is a terminal-native git history viewer: instant startup, visual branch topology, keyboard-driven navigation. It fills the gap between git log and full git clients like lazygit - a focused tool for browsing commit history, similar to VS Code Git Graph but available everywhere.

My personal workflow involves frequent releases through CI/CD, which means constant tag management. Serie excels at navigating history and finding the right commit, but creating a tag still requires a context switch: copy the hash, open terminal, run git tag, then git push --tags. Even lazygit doesn't solve this completely - it can create tags but won't push them to origin.

This PR adds complete ref management to Serie: create/delete tags and branches, push to remote - all without leaving the interface. Tags and branches are a natural fit for a history viewer. They don't modify history, they're reversible; "tag this commit" or "delete this stale branch" are logical next steps after finding the right ref.

Features

Tag Management (Commit List)

  • t - create tag on selected commit
  • Ctrl-T - delete tag from selected commit
  • Create dialog: name input, optional message, push-to-origin checkbox
  • Delete dialog: lists all tags on commit, remote removal option
  • Semver-aware sorting (v1.2.10 appears after v1.2.9, not between v1.2.1 and v1.2.2)

Ref Management (Refs Panel)

  • d - delete selected branch or tag
  • Branch deletion with force option (for unmerged branches)
  • Remote deletion support for both branches and tags
  • Confirmation dialog with clear remote/local indicators

Pending Overlay

  • Blocks input during git operations
  • Displays operation status
  • Prevents double-submission

Search & Filter

  • Alt-C - toggle case-insensitive search (changed from Ctrl-G to match industry standard)
  • Ctrl-X - toggle fuzzy matching
  • Cache invalidates when ignore_case/fuzzy settings change mid-query

Hotkey Hints

  • Context-aware keybindings in footer
  • Updates based on current view (list, detail, refs, dialogs)
  • Reflects custom bindings from config

Refresh

  • r - reloads repository state

Bug Fixes

  • Panic prevention: Guards for empty list/height in all navigation methods
  • Search + filter conflict: search_matches used real indices but total was filtered count, causing index out of bounds
  • Index conversion: Added is_index_visible() and select_real_index() helpers

Performance

  • Fixed O(n²) in refs_to_ref_tree_nodes - now O(n)

Technical Notes

Rc Migration: Originally, &Commit and &Ref were passed as borrowed references — zero-cost, but inflexible. The problem: the tag creation dialog needs to hold commit data, but the commit list may refresh while the dialog is open. Rc<T> solves this — cloning just copies the pointer and increments the reference count, with no deep data copying.

Why this is a good pattern:

  • Ownership decoupling — UI components become independent of the data source lifecycle. The dialog owns its data and doesn't care if the parent refreshes.
  • Cheap clonesRc::clone() is O(1), just a pointer copy + atomic increment. No allocation, no memcpy of commit messages, hashes, etc.
  • Immutable by defaultRc<T> gives shared read-only access. If mutation is needed, Rc<RefCell<T>> makes it explicit.
  • Idiomatic for GUI — ratatui and most Rust TUI frameworks expect owned data in widget state. Fighting the borrow checker with lifetimes in UI code leads to complexity; Rc is the standard escape hatch.
  • Predictable cleanup — when the dialog closes, its Rc drops. If no other references exist, memory is freed. No manual cleanup, no dangling pointers.

Trade-off: Small runtime cost (refcount operations) vs. significant code simplicity. For UI data that's read frequently but rarely mutated, this is the right balance.

Ownership Change: App now owns Repository instead of borrowing it, enabling mutation for add_ref/remove_ref after tag/branch operations.

How Has This Been Tested?

Test repository generation with scripts/generate_test_repo.sh:
./scripts/generate_test_repo.sh /tmp/test-repo 10000
Creates realistic repository with feature/bugfix branches, merge commits, semver tags, multiple authors.

Tested scenarios:

  • Tag create/delete with push to origin
  • Branch delete (local, remote, force)
  • Search + filter combinations with settings toggle
  • Refresh during various view states
  • Navigation edge cases (empty list, single commit, filtered to zero results)

I know the diff is large, but most changes are straightforward — tests pass and lints are clean. I believe this enriches the project without changing its core concept and principles. Happy to walk through any part if needed.

shot_251218_172437 shot_251218_172504 shot_251218_172529 shot_251218_172851

  - Create tag dialog: name input, optional message, push to origin checkbox
  - Delete tag dialog: select from list, delete from remote option
  - Tags sorted with semver awareness
  - UI updates immediately after tag operations
  - Add guards for empty list/height in navigation methods (select_next,
    select_last, select_low, select_middle, scroll_up, scroll_down_height,
    select_parent, select_next_match, select_prev_match)
  - Fix search navigation when filter is active: search_matches uses real
    indices but total was filtered count, causing index out of bounds
  - Add is_index_visible() and select_real_index() helpers for proper
    index conversion between filtered and full commit lists
  - Track last_search_ignore_case/fuzzy to invalidate incremental search
    cache when settings change
  - Show relevant hotkeys based on current view (list, detail, refs, etc.)
  - Change ignore_case_toggle keybind from Ctrl-g to Alt-c (industry standard)
  - App owns Repository instead of borrowing (enables mutation)
  - Add add_ref/remove_ref methods to Repository for ref tracking
  - take_list_state() now returns Option, views handle gracefully
  - event.rs: channel errors handled without panic
  - ref_list: show error message on tree build failure
  - ref_list: fix O(n²) performance in refs_to_ref_tree_nodes
@lusingander
Copy link
Owner

Thank you for your interest in this project and for taking the time to put together this PR.

As you mentioned, I’ve also thought that having branch and tag operations in this application could make sense, so at a high level I agree that the idea itself is reasonable.

That said, for the reasons below, I’m hesitant to accept this PR in its current form.

UI concerns

This PR introduces dialog-based UIs for each operation. I understand that some kind of UI is necessary for branch and tag management, but I think this needs more careful consideration. One of the goals of this application is to avoid becoming a full-featured or complex TUI git client. For that reason, I’ve been intentionally avoiding “TUI-style” interactions so far.
I’d like to keep the behavior and interaction model consistent with existing features, and avoid introducing things like checkbox-based interactions or in-app help panels that would break that consistency.

Scope of changes

This PR also includes a wide range of changes, such as reload functionality, filtering features, performance optimizations, and changes to the default key bindings. While I understand that some of these (for example, reload support) may be necessary to implement the proposed features, I don’t think they should all be introduced in a single PR.

I also think filtering should be discussed and designed after we’ve clarified how it affects graph rendering. Elements like the hints displayed at the bottom of the screen relate to both UI and feature scope, and their current behavior is also a deliberate choice to avoid a more TUI-heavy interface (though I’m not claiming this is necessarily the best approach).

For these reasons, I’m not comfortable merging this PR as-is. I appreciate the effort and ideas, and I think it would be better to discuss the design and scope in smaller, more focused steps.

- Add graph_color_set and cell_width_type fields to App struct
- Reload Repository from disk on refresh
- Recalculate Graph and rebuild GraphImageManager
- Rebuild CommitListState with updated commits and refs
- Show success/error notification after refresh
…me input

Use char-based slicing instead of byte-based slicing when truncating
long input values for display.
- Update UI when local deletion succeeds but remote fails
- Use safe bounds checking with .get() in delete_tag
@alfatm
Copy link
Author

alfatm commented Dec 21, 2025

Thank you for the feedback.

I'm glad we agree that branch and tag operations would be a valuable addition to the project — that's a solid foundation to build on. These changes are deliberate, well-considered, and aligned with what has become standard practice in successful applications in this space.

UI Concerns

Regarding the UI concerns: to be honest, I'm not sure what alternative approach you have in mind. Branch and tag operations inherently require some form of interactive UI — I don't see how to implement them otherwise.

The current implementation follows ui patterns established by the most popular tools in this space, particularly Git Graph for VS Code, which has become something of an industry standard with millions of users.

Clarification Needed

I'm also not entirely clear on what "TUI-heavy interface" means in this context.
Is there a design document, style guide, or reference implementation that describes the interaction model you're aiming for?
Without a concrete alternative, "avoiding TUI patterns" sounds less like a design philosophy and more like simply not having a UI at all — which isn't really a viable approach for features that require user input and decision-making.

If you have a specific vision for how these interactions should work, I'd genuinely like to understand it. I'm open to iterating on the implementation, but I'd need something concrete to work towards.

Next Steps

I've put together a detailed list of all the changes below. Could you:

  1. Go through it and let me know which parts you're comfortable with
  2. Identify which ones need changes

That way we can reduce the scope and have a focused discussion on specific points. I'm also happy to split this into smaller PRs if that makes the review easier.

Let me know how you'd like to proceed.
A discussion thread might be a good place to align on this before I rework anything.

Tag Management - Create Tag Dialog

Files: src/view/create_tag.rs, src/app.rs, src/event.rs, src/git.rs

  • Dialog UI component (CreateTagView)

    • Form with 3 fields: Tag name, Message, Push checkbox
    • Tab/Shift-Tab navigation between fields
    • Arrow key navigation (up/down)
    • Backspace handling in input mode
    • Cursor positioning in text inputs
    • Input truncation display for long values (render_input_field)
    • Default: push to remote = true
  • Input field component (FocusedField enum)

    • TagName, Message, PushCheckbox states
    • Visual focus highlighting (bold + green)
    • Checkbox toggle with Space/Left/Right keys
  • Git operations (git.rs)

    • create_tag() - lightweight and annotated tags
    • push_tag() - push to origin
    • validate_ref_name() - ref name validation
  • Event system (event.rs)

    • OpenCreateTag event
    • CloseCreateTag event
    • AddTagToCommit { commit_hash, tag_name } event
    • CreateTag user event
  • Background execution

    • thread::spawn for git operations
    • Show pending overlay during operation
    • Close dialog immediately, run git in background
    • Handle partial success (local ok, push failed)

Tag Management - Delete Tag Dialog

Files: src/view/delete_tag.rs, src/app.rs, src/event.rs, src/git.rs

  • Dialog UI component (DeleteTagView)

    • List of tags on selected commit
    • Arrow key selection in list
    • "Delete from origin" checkbox
    • Show commit hash context
  • Semver sorting (compare_semver)

    • Parse version strings (v1.2.3, 1.2.3-beta)
    • Numeric comparison for major.minor.patch
    • Suffix handling (pre-release)
    • Non-semver tags sorted alphabetically
  • Git operations (git.rs)

    • delete_tag() - local deletion
    • delete_remote_tag() - origin deletion
  • Event system (event.rs)

    • OpenDeleteTag event
    • CloseDeleteTag event
    • RemoveTagFromCommit { commit_hash, tag_name } event
    • DeleteTag user event

Delete Refs from Refs Panel

Files: src/view/delete_ref.rs, src/view/refs.rs, src/widget/ref_list.rs, src/git.rs

  • Unified ref deletion dialog (DeleteRefView)

    • Support for Tags, Branches, Remote Branches
    • Context-aware checkbox ("Delete from origin" / "Force delete")
    • Dynamic dialog title based on ref type
  • Branch deletion (git.rs)

    • delete_branch() - safe deletion (-d)
    • delete_branch_force() - force deletion (-D)
    • delete_remote_branch() - origin deletion (parses "remote/branch" format)
  • RefType enum (git.rs)

    • Tag, Branch, RemoteBranch variants
  • Event system (event.rs)

    • OpenDeleteRef { ref_name, ref_type } event
    • CloseDeleteRef event
    • RemoveRefFromList { ref_name } event
  • Refs view integration (refs.rs)

    • d key to open delete dialog
    • open_delete_ref() method
    • Detection of selected item type (local/remote branch, tag)
  • Selection adjustment (ref_list.rs)

    • adjust_selection_after_delete() method
    • Try next item first, then previous

Pending Overlay Component

Files: src/widget/pending_overlay.rs, src/app.rs, src/event.rs

  • Widget implementation (PendingOverlay)

    • Centered dialog with "Working..." title
    • Word-wrapped message text
    • "Esc" hint to hide
    • Dynamic height based on content
    • Handle long words (split into chunks)
  • Text wrapping (wrap_text)

    • Word-by-word wrapping
    • Respect max width
    • Split words longer than max_width
  • App integration (app.rs)

    • pending_message: Option<String> state
    • Render overlay on top of main view
    • Block keyboard input while pending
    • Esc hides overlay (operation continues)
  • Event system (event.rs)

    • ShowPendingOverlay { message } event
    • HidePendingOverlay event

Context-Aware Hotkey Hints in Footer

Files: src/app.rs

  • View-specific hints (build_hotkey_hints)

    • List view: search, filter, case, tag, refs, refresh, help
    • Detail view: copy, close, help
    • Refs view: copy, delete, close, help
    • Dialog views: confirm, cancel
    • Help view: close
  • Dynamic key lookup

    • keybind.keys_for_event() to get actual bound keys
    • Display "key description" format

Keybind Changes

Files: assets/default-keybind.toml

  • ignore_case_toggle: Ctrl-gAlt-c
  • create_tag: t
  • delete_tag: Ctrl-t
  • refresh: r or F5

Search/Filter Index Conflict Fix

Files: src/widget/commit_list.rs

  • Empty list guards

    • select_next() - check total == 0 || height == 0
    • select_last() - check total == 0 || height == 0
    • select_low() - check total == 0 || height == 0
    • select_middle() - check total == 0
    • scroll_up() - check height == 0
    • scroll_down_height() - check total == 0 || height == 0
    • select_parent() - check total == 0
    • select_next_match() - check commits.is_empty()
    • select_prev_match() - check commits.is_empty()
  • Real vs visible index conversion

    • is_index_visible() - check if real index in filtered_indices
    • select_real_index() - convert real to visible index
    • current_selected_index() - get real index for visible selection
    • real_commit_index() - map visible → real
  • Search cache invalidation

    • last_search_ignore_case field
    • last_search_fuzzy field
    • Check settings unchanged before incremental search

Error Handling & Panic Removal

Files: src/app.rs, src/event.rs, src/git.rs, src/widget/ref_list.rs

  • App ownership (app.rs)

    • App owns Repository (not borrowing)
    • repository: Repository field (not reference)
  • Repository mutation (git.rs)

    • add_ref(&mut self, new_ref: Ref) method
    • remove_ref(&mut self, ref_name: &str) method
    • In-memory ref_map updates
  • Graceful channel handling (event.rs)

    • Sender::send() - ignore send errors (let _ = self.tx.send(event))
    • Receiver::recv() - return Quit on error (unwrap_or(AppEvent::Quit))
  • Optional list state (all views)

    • take_list_state() returns Option<CommitListState>
    • Views handle None gracefully with early return
  • Ref tree error handling (ref_list.rs)

    • Tree::new() returns Result
    • Show error message paragraph on failure

Performance Fix - O(n²) → O(n)

Files: src/widget/ref_list.rs

  • refs_to_ref_tree_nodes optimization
    • Use position-based lookup instead of nested iteration
    • Build parent identifier incrementally

Refresh Button (Full Implementation)

Files: src/app.rs, src/event.rs, assets/default-keybind.toml

  • Refresh event (event.rs)

    • Refresh app event
    • Refresh user event
  • App handling (app.rs)

    • refresh() method - fully implemented
    • Reload Repository from disk
    • Recalculate Graph
    • Rebuild GraphImageManager
    • Rebuild CommitListState
    • Show success/error notification
  • New App fields

    • graph_color_set: &'a GraphColorSet
    • cell_width_type: CellWidthType

Rc Refactoring

Files: src/view/*.rs, src/widget/*.rs

  • Commit refs - Vec<Rc<Ref>> instead of Vec<Ref>
  • Shared ref ownership across views

UX Improvements

Files: src/widget/pending_overlay.rs, src/widget/ref_list.rs, src/git.rs

  • Word wrapping fix (pending_overlay.rs)

    • Split long words into chunks
    • Handle max_width == 0 edge case
    • Use chars().count() for Unicode
  • Consistent tag sorting (ref_list.rs)

    • All tags sorted descending (newest first)
    • Semver tags before non-semver
    • Non-semver sorted Z-A
  • Git ref name validation (git.rs)

    • validate_ref_name() function
    • Checks: empty, starts with -/., contains ..////@{/\
    • Forbidden: control chars, spaces, ~, ^, :
    • Forbidden endings: /, ., .lock

Test Script

Files: scripts/generate_test_repo.sh

  • Generate 10k commits
  • Create multiple branches
  • Create semantic version tags
  • Realistic commit patterns

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants