restructure
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s

This commit is contained in:
2026-09-12 18:26:06 +07:00
parent f9f0d33bb1
commit d68c098818
24 changed files with 3177 additions and 2924 deletions
+237
View File
@@ -0,0 +1,237 @@
use std::path::PathBuf;
use std::rc::Rc;
use anyhow::Error;
use dock::{add_center_panel, panel_handle};
use gpui::prelude::*;
use gpui::{AnyElement, Context, Window, div, px, size};
use gpui_component::scroll::Scrollbar;
use gpui_component::spinner::Spinner;
use gpui_component::{ActiveTheme, Sizable, v_flex, v_virtual_list};
use signed_ui::placeholder;
use super::RepoDetailView;
use crate::views::commit_diff::CommitDiffView;
use crate::views::repo::helpers::{COMMIT_ROW_HEIGHT, commit_row};
impl RepoDetailView {
pub(super) fn render_commits_tab(&self, cx: &mut Context<Self>) -> AnyElement {
let Some(list) = self.all_commits.as_ref() else {
return if self.loading_all_commits {
v_flex()
.size_full()
.items_center()
.justify_center()
.child(Spinner::new().small())
.into_any_element()
} else {
placeholder("Failed to load commits", cx)
};
};
if list.commits.is_empty() {
return placeholder("No commits found", cx);
}
// Copy only the values the element tree needs.
// The list is borrowed by the renderer below instead of cloned per frame.
// A full history can be tens of thousands of commits.
let view = cx.entity().clone();
let sizes = self.item_sizes.clone();
let scroll_handle = self.scroll_handle.clone();
let shown = list.commits.len();
let total = list.total;
v_flex()
.relative()
.flex_1()
.w_full()
.min_h_0()
.child(
v_virtual_list(view, "commits", sizes, move |this, range, _window, cx| {
let view = cx.entity().downgrade();
let commits = this
.all_commits
.as_ref()
.map(|list| list.commits.as_slice())
.unwrap_or(&[]);
range
.map(|ix| {
let id = commits[ix].id.clone();
let view = view.clone();
commit_row(
ix,
&commits[ix],
move |window, cx| {
if let Some(view) = view.upgrade() {
view.update(cx, |this, cx| {
this.open_commit_diff(&id, window, cx)
});
}
},
cx,
)
})
.collect()
})
.track_scroll(&scroll_handle)
.size_full(),
)
.when(shown < total, |this| {
// The history is capped.
// Tell the user the list is truncated.
this.child(
div()
.py_2()
.w_full()
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(format!("Showing {shown} of {total} commits")),
)
})
.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::vertical(&self.scroll_handle)),
)
.into_any_element()
}
}
impl RepoDetailView {
/// Queue `path` for the per-file commit query.
/// Requests are batched into one history walk, see [`Self::load_commits`].
pub(super) fn load_commit(&mut self, path: &str, cx: &mut Context<Self>) {
if self.commits.contains_key(path) || self.pending_commits.iter().any(|p| p == path) {
return;
}
self.pending_commits.push(path.to_string());
if !self.loading_commits {
self.load_commits(cx);
}
}
/// Walk history once for every queued path on a background task.
/// Cache the latest commit touching each path in [`Self::commits`].
/// That feeds the file header in the content column.
/// Batching shares one walk across paths queued while the previous walk ran.
fn load_commits(&mut self, cx: &mut Context<Self>) {
if self.pending_commits.is_empty() || self.loading_commits {
return;
}
let Some(worktree) = self.worktree.clone() else {
self.pending_commits.clear();
return;
};
self.loading_commits = true;
let paths = std::mem::take(&mut self.pending_commits);
let generation = self.ref_generation;
let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let rels: Vec<PathBuf> = paths.iter().map(PathBuf::from).collect();
let result = cx
.background_spawn(
async move { signed_git::worktree_last_commits(&worktree, &rels) },
)
.await;
this.update(cx, |this, cx| {
this.loading_commits = false;
if generation == this.ref_generation
&& let Ok(found) = result
{
for (path, commit) in found {
this.commits
.insert(path.to_string_lossy().into_owned(), commit);
}
}
// Paths queued while the walk was in flight start the next batch.
// A stale walk, branch switched mid-flight, must not strand them.
// This runs under the current generation regardless of the result.
if !this.pending_commits.is_empty() {
this.load_commits(cx);
}
cx.notify();
})?;
Ok(())
});
task.detach();
}
/// Walk all commits reachable from HEAD on a background task.
/// For the Commits tab and its total-count badge.
/// [`CommitList`] caps the list, only the newest commits are materialized.
pub(super) fn load_all_commits(&mut self, cx: &mut Context<Self>) {
if self.loading_all_commits || self.all_commits.is_some() {
return;
}
let Some(worktree) = self.worktree.clone() else {
return;
};
self.loading_all_commits = true;
let generation = self.ref_generation;
let task: gpui::Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
let result = cx
.background_spawn(async move { signed_git::worktree_all_commits(&worktree) })
.await;
this.update(cx, |this, cx| {
// A stale walk, branch switched mid-flight, must not leave the flag set.
// Otherwise the Commits tab would spin forever.
if generation != this.ref_generation {
this.loading_all_commits = false;
return;
}
if let Ok(list) = result {
let count = list.commits.len();
this.item_sizes = Rc::new(vec![size(px(0.), px(COMMIT_ROW_HEIGHT)); count]);
this.all_commits = Some(list);
}
this.loading_all_commits = false;
cx.notify();
})?;
Ok(())
});
task.detach();
}
/// Open a new panel showing the diff of `commit_id`.
pub(super) fn open_commit_diff(
&mut self,
commit_id: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(worktree) = self.worktree.clone() else {
return;
};
let Some(dock_area) = self.dock_area.upgrade() else {
return;
};
// Same display name as the repo detail panel's title.
let repo_name = self.display_name(cx);
let panel =
cx.new(|cx| CommitDiffView::new(worktree, repo_name, commit_id.into(), window, cx));
dock_area.update(cx, |dock_area, cx| {
add_center_panel(dock_area, panel_handle(panel), window, cx);
});
}
}