Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,33 @@ hint bar는 오버레이(repo 입력·prefix armed·swap target)가 열리면

snapshot worker는 매 폴 사이클마다 현재 HEAD oid를 함께 보고한다. UI 스레드는 `poll_snapshot`에서 oid 변동을 감지하면 `refresh_commit_log_after_head_change`로 commit log와 drill-down 상태를 동일 oid 기준으로 재정렬해, 터미널에서 새 커밋·amend·force-push·브랜치 전환이 일어났을 때도 로그 뷰가 즉시 따라잡는다.

### Commit Log Decoration

`git log --decorate`가 주는 방향 감각을 로그 뷰에 옮긴 것이다. `src/git/diff/refs.rs`가
`repo.references()`를 한 번 걸어 `Oid -> Vec<RefLabel>` 맵을 만들고, HEAD·로컬 브랜치·태그·
원격 브랜치를 구분해 커밋 행에 chip으로 그린다. 비용은 커밋 수가 아니라 **ref 수**에
비례하고, annotated tag은 `peel_to_commit`으로 가리키는 커밋에 붙인다.

- **재생성 시점은 refs fingerprint가 정한다**: fetch가 `origin/dev`를 옮기면 HEAD는
그대로여도 chip은 달라져야 한다. snapshot worker가 매 폴마다 ref 이름·타깃의 다이제스트를
`RepoSnapshot::refs_fingerprint`로 실어 보내고, UI 스레드는 그 값이 바뀔 때만 맵을 다시
만든다. 재생성 실패는 이전 맵을 유지한다 — 일시적 읽기 오류로 chip이 사라지는 것보다
낫다.
- **ahead/behind는 위치가 아니라 oid 집합으로 판정한다**: 이전 구현은 "위에서 N개가
ahead"라는 위치 가정이었고, anchor가 HEAD가 아니거나 필터가 걸리면 마커가 엉뚱한 행에
붙었다. 지금은 `revwalk.push(local)` + `hide(upstream)`(과 그 반대)로 각 방향의 oid
집합을 만들어 멤버십으로 판정한다. 집합은 방향당 `MAX_DIVERGENCE_OIDS`개로 끊는다 —
walk가 최신순이므로 잘리는 쪽은 화면에 닿지 않는 꼬리다.
- **1 커밋 = 1 행을 유지한다**: `log_view.selected`가 커밋 인덱스이자 화면 위치라는 전제를
선택·스크롤·tail prefetch가 공유한다. 여유 공간은 행이 아니라 **컬럼**으로 쓴다.
`area.width >= MIN_DETAIL_WIDTH`이면 상대 시각 대신 절대 시각, author에 email, short_id
10자, chip 무절단으로 넓힌다. 판정 기준이 `list_fullscreen` 플래그가 아니라 폭인 이유는
넓은 모니터에서는 fullscreen이 아니어도 자리가 남기 때문이고, 이는
`diff_viewer::MIN_SPLIT_WIDTH`가 이미 세운 선례와 같은 모양이다.
- **commit graph는 범위 밖이다**: lane graph는 topological 정렬을 전제하는데 현재 revwalk에는
`set_sorting`이 없고, 정렬을 바꾸면 위의 anchor+skip 페이지네이션 계약까지 함께 다시
설계해야 한다.

### Plugin Host (`src/plugin/`, `plugins/`)

어떤 CLI가 사용량 한도에 걸렸는지 알아보고 한도가 풀린 뒤 세션을 재개하는 일은 provider를
Expand Down
5 changes: 5 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ pub struct App {
pub list_fullscreen: bool,
// `None` for detached HEAD / unborn branch / bare repo.
pub branch_name: Option<String>,
// Ref chips and ahead/behind sets for the Log view. Rebuilt only when
// `last_refs_fingerprint` disagrees with the newest snapshot's, so a fetch
// that moves `origin/dev` refreshes it but an idle poll does not.
pub log_decorations: crate::git::diff::LogDecorations,
pub(crate) last_refs_fingerprint: Option<u64>,
pub leader: KeyEvent,
// No timeout: stays armed until a follow-up key or `Esc`/`Ctrl+C` resolves it.
pub prefix_armed: bool,
Expand Down
2 changes: 2 additions & 0 deletions src/app/app_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ impl App {
auto_follow: AutoFollow::default(),
list_fullscreen: false,
branch_name: None,
log_decorations: Default::default(),
last_refs_fingerprint: None,
leader,
prefix_armed: false,
awaiting_swap_target: false,
Expand Down
17 changes: 17 additions & 0 deletions src/app/snapshot_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ impl App {
});
let new_head = snapshot.head_oid;
self.branch_name = snapshot.branch_name;
self.refresh_log_decorations(snapshot.refs_fingerprint);
self.status_view.set_files(snapshot.files);
self.status_view.recompute_filter();
self.tracking = snapshot.tracking;
Expand Down Expand Up @@ -87,6 +88,22 @@ impl App {
}
}

// Rebuilding walks every ref and peels each one, so it is gated on the
// fingerprint rather than run per poll. A failure leaves the previous map in
// place: stale chips beat chips vanishing on a transient read error.
fn refresh_log_decorations(&mut self, fingerprint: u64) {
if self.last_refs_fingerprint == Some(fingerprint) {
return;
}
match self.with_repo(crate::git::diff::load_log_decorations) {
Ok(decorations) => {
self.log_decorations = decorations;
self.last_refs_fingerprint = Some(fingerprint);
}
Err(e) => tracing::warn!(error = %e, "failed to load ref decorations"),
}
}

// A path whose previous mtime was newer than the freshly observed one keeps
// its previous mtime — a rename-from-stash can resurrect older mtimes for
// the same path and must not demote a recent edit to cool. Updates in place
Expand Down
1 change: 1 addition & 0 deletions src/app/tests/auto_follow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fn snapshot_with(paths: &[&str]) -> RepoSnapshot {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
}
}

Expand Down
1 change: 1 addition & 0 deletions src/app/tests/diff_file_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ fn snapshot_refresh_with_no_filter_matches_clears_file_view() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down
1 change: 1 addition & 0 deletions src/app/tests/head_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ fn snapshot_with_head(repo_path: &str) -> RepoSnapshot {
tracking: None,
head_oid: head,
branch_name: None,
refs_fingerprint: 0,
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/app/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub(crate) fn app_with_files(files: Vec<&str>) -> App {
auto_follow: AutoFollow::default(),
list_fullscreen: false,
branch_name: None,
log_decorations: Default::default(),
last_refs_fingerprint: None,
leader: KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL),
prefix_armed: false,
awaiting_swap_target: false,
Expand Down
3 changes: 3 additions & 0 deletions src/app/tests/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ fn drain_snapshot_empties_the_queue_without_applying_it() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
)
Expand Down Expand Up @@ -65,6 +66,7 @@ fn a_saved_mode_lands_immediately_and_survives_being_changed() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down Expand Up @@ -100,6 +102,7 @@ fn a_saved_selection_is_restored_by_the_first_snapshot() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down
5 changes: 5 additions & 0 deletions src/app/tests/snapshot_refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fn successful_snapshot_preserves_terminal_status() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down Expand Up @@ -48,6 +49,7 @@ fn successful_snapshot_clears_git_status() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down Expand Up @@ -77,6 +79,7 @@ fn snapshot_refresh_clamps_selection_to_active_filter() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down Expand Up @@ -113,6 +116,7 @@ fn snapshot_invalidates_path_width_cache_on_same_length_rename() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down Expand Up @@ -147,6 +151,7 @@ fn snapshot_refresh_with_no_filter_matches_clears_stale_diff() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down
1 change: 1 addition & 0 deletions src/app/tests/tree_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ fn tree_preview_survives_status_snapshot() {
tracking: None,
head_oid: None,
branch_name: None,
refs_fingerprint: 0,
},
HashMap::new(),
))
Expand Down
2 changes: 2 additions & 0 deletions src/git/diff.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod commit_log;
mod diff_load;
mod refs;
mod snapshot;
mod types;

Expand All @@ -12,6 +13,7 @@ pub use diff_load::{
load_commit_diff, load_commit_file_blob, load_commit_file_diff, load_commit_files,
load_file_diff, load_workdir_file, parse_hunk_new_start,
};
pub use refs::{LogDecorations, RefKind, RefLabel, load_log_decorations};
pub use snapshot::load_snapshot;
#[cfg(test)]
pub use types::DiffLine;
Expand Down
9 changes: 7 additions & 2 deletions src/git/diff/commit_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,14 @@ pub fn load_commit_log_from(
let oid = oid_result.context("revwalk error")?;
let commit = repo.find_commit(oid).context("failed to find commit")?;
let summary = commit.summary().ok().flatten().unwrap_or("").to_string();
let author = commit.author().name().unwrap_or("Unknown").to_string();
let signature = commit.author();
let author = signature.name().unwrap_or("Unknown").to_string();
let email = signature.email().unwrap_or("").to_string();
let time = commit.time().seconds();
entries.push(CommitEntry::new(oid, short_oid(oid), summary, author, time));
entries.push(
CommitEntry::new(oid, short_oid(oid), summary, author, time)
.with_commit_meta(email, commit.parent_count()),
);
}
Ok(entries)
}
Expand Down
Loading