Merge efc009ee65
into 0e575b2809
This commit is contained in:
commit
1f6ba60412
12 changed files with 425 additions and 2 deletions
|
@ -760,7 +760,11 @@
|
|||
// Choices: always, auto, never, system
|
||||
// Default: inherits editor scrollbar settings
|
||||
// "show": null
|
||||
}
|
||||
},
|
||||
//// Whether to show a list of previous commits in the git panel.
|
||||
///
|
||||
/// Default: false
|
||||
"commit_history": false
|
||||
},
|
||||
"message_editor": {
|
||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
|
|
|
@ -464,6 +464,7 @@ impl Server {
|
|||
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitLog>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LoadCommitDiff>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
|
||||
|
|
|
@ -156,6 +156,15 @@ impl GitRepository for FakeGitRepository {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn log(&self, _skip: u64, _max_count: u64) -> BoxFuture<'_, Result<Vec<CommitDetails>>> {
|
||||
async {
|
||||
Ok(vec![CommitDetails {
|
||||
..Default::default()
|
||||
}])
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn reset(
|
||||
&self,
|
||||
_commit: String,
|
||||
|
|
|
@ -359,6 +359,10 @@ pub trait GitRepository: Send + Sync {
|
|||
|
||||
fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>>;
|
||||
|
||||
/// Returns the git commit log returning at most `max_count` commits from
|
||||
/// the current branch, skipping `skip` results
|
||||
fn log(&self, skip: u64, max_count: u64) -> BoxFuture<'_, Result<Vec<CommitDetails>>>;
|
||||
|
||||
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>>;
|
||||
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>>;
|
||||
|
||||
|
@ -614,6 +618,57 @@ impl GitRepository for RealGitRepository {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn log(&self, skip: u64, max_size: u64) -> BoxFuture<'_, Result<Vec<CommitDetails>>> {
|
||||
let working_directory = self.working_directory();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let working_directory = working_directory?;
|
||||
let output = new_std_command("git")
|
||||
.current_dir(&working_directory)
|
||||
.args([
|
||||
"--no-pager",
|
||||
"--no-optional-locks",
|
||||
"log",
|
||||
"--skip",
|
||||
&skip.to_string(),
|
||||
"--max-count",
|
||||
&max_size.to_string(),
|
||||
"--format=%H%x00%B%x00%at%x00%ae%x00%an%x00",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let output = std::str::from_utf8(&output.stdout)?
|
||||
.trim()
|
||||
.trim_end_matches('\0');
|
||||
let parts: Vec<&str> = output.split('\0').map(|value| value.trim()).collect();
|
||||
|
||||
if parts.len() % 5 != 0 {
|
||||
bail!("unexpected git log output length: {}", parts.len());
|
||||
}
|
||||
|
||||
let mut commits = Vec::with_capacity(parts.len() / 5);
|
||||
|
||||
for chunk in parts.chunks(5) {
|
||||
let sha = chunk[0].to_string().into();
|
||||
let message = chunk[1].to_string().into();
|
||||
let commit_timestamp = chunk[2].parse()?;
|
||||
let author_email = chunk[3].to_string().into();
|
||||
let author_name = chunk[4].to_string().into();
|
||||
|
||||
commits.push(CommitDetails {
|
||||
sha,
|
||||
message,
|
||||
commit_timestamp,
|
||||
author_email,
|
||||
author_name,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(commits)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> {
|
||||
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
|
||||
else {
|
||||
|
|
285
crates/git_ui/src/git_commit_list.rs
Normal file
285
crates/git_ui/src/git_commit_list.rs
Normal file
|
@ -0,0 +1,285 @@
|
|||
use crate::commit_view::CommitView;
|
||||
use git::{blame::ParsedCommitMessage, repository::CommitSummary};
|
||||
use gpui::{
|
||||
App, Entity, ListScrollEvent, ListState, MouseButton, ParentElement, Render, Stateful, Task,
|
||||
WeakEntity, list, prelude::*,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use project::{
|
||||
Project,
|
||||
git_store::{GitStoreEvent, Repository},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use ui::{ListItem, Scrollbar, ScrollbarState, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
const COMMITS_PER_PAGE: u64 = 20;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommitDetails {
|
||||
pub sha: SharedString,
|
||||
pub author_name: SharedString,
|
||||
pub author_email: SharedString,
|
||||
pub commit_time: OffsetDateTime,
|
||||
pub message: Option<ParsedCommitMessage>,
|
||||
}
|
||||
|
||||
pub struct GitCommitList {
|
||||
pub(crate) active_repository: Option<Entity<Repository>>,
|
||||
pub(crate) project: Entity<Project>,
|
||||
pub(crate) workspace: WeakEntity<Workspace>,
|
||||
|
||||
commits: Vec<CommitDetails>,
|
||||
commits_loading: bool,
|
||||
|
||||
commits_list: ListState,
|
||||
scrollbar_state: ScrollbarState,
|
||||
}
|
||||
|
||||
impl GitCommitList {
|
||||
pub fn new(workspace: &mut Workspace, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let project = workspace.project().clone();
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
let active_repository = project.read(cx).active_repository(cx);
|
||||
let workspace = workspace.weak_handle();
|
||||
|
||||
cx.new(|cx| {
|
||||
cx.subscribe_in(
|
||||
&git_store,
|
||||
window,
|
||||
move |this: &mut GitCommitList, _git_store, event, window, cx| match event {
|
||||
GitStoreEvent::ActiveRepositoryChanged(_) => {
|
||||
this.active_repository = this.project.read(cx).active_repository(cx);
|
||||
this.reload_history(window, cx)
|
||||
}
|
||||
|
||||
// Reload the git history on repository changes to the current repo
|
||||
GitStoreEvent::RepositoryUpdated(_, _, true) => this.reload_history(window, cx),
|
||||
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let commits_list = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
|
||||
commits_list.set_scroll_handler(cx.listener(
|
||||
|this: &mut Self, event: &ListScrollEvent, window, cx| {
|
||||
if this.commits.len() > 5
|
||||
&& event.visible_range.end >= this.commits.len() - 5
|
||||
&& this.has_next_page()
|
||||
{
|
||||
this.load_next_history_page(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
let scrollbar_state =
|
||||
ScrollbarState::new(commits_list.clone()).parent_entity(&cx.entity());
|
||||
|
||||
let this = Self {
|
||||
active_repository,
|
||||
project,
|
||||
workspace,
|
||||
commits: Vec::new(),
|
||||
commits_loading: false,
|
||||
commits_list,
|
||||
scrollbar_state,
|
||||
};
|
||||
|
||||
this.load_next_history_page(window, cx);
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
fn reload_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.commits.clear();
|
||||
self.commits_loading = false;
|
||||
self.commits_list.reset(0);
|
||||
|
||||
self.load_next_history_page(window, cx);
|
||||
}
|
||||
|
||||
// We probabbly have a next page if the length of all pages matches the per page amount
|
||||
fn has_next_page(&self) -> bool {
|
||||
self.commits.len() % (COMMITS_PER_PAGE as usize) == 0
|
||||
}
|
||||
|
||||
fn load_next_history_page(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Skip if already loading a page
|
||||
if self.commits_loading {
|
||||
return;
|
||||
}
|
||||
|
||||
let skip = self.commits.len() as u64;
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let details = this.update(cx, |list: &mut GitCommitList, cx| {
|
||||
list.commits_loading = true;
|
||||
list.load_commit_history(cx, skip, COMMITS_PER_PAGE)
|
||||
})?;
|
||||
|
||||
let details = details.await?;
|
||||
|
||||
let commits: Vec<CommitDetails> = details
|
||||
.into_iter()
|
||||
.map(|commit| {
|
||||
anyhow::Ok(CommitDetails {
|
||||
sha: commit.sha.clone(),
|
||||
author_name: commit.author_name.clone(),
|
||||
author_email: commit.author_email.clone(),
|
||||
commit_time: OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)?,
|
||||
message: Some(ParsedCommitMessage {
|
||||
message: commit.message,
|
||||
..Default::default()
|
||||
}),
|
||||
})
|
||||
})
|
||||
.try_collect()?;
|
||||
|
||||
this.update(cx, |this: &mut GitCommitList, cx| {
|
||||
let count = commits.len();
|
||||
let current_count = this.commits_list.item_count();
|
||||
|
||||
this.commits_loading = false;
|
||||
this.commits.extend(commits);
|
||||
this.commits_list
|
||||
.splice(current_count..current_count, count);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn load_commit_history(
|
||||
&self,
|
||||
cx: &mut Context<Self>,
|
||||
skip: u64,
|
||||
max_count: u64,
|
||||
) -> Task<anyhow::Result<Vec<git::repository::CommitDetails>>> {
|
||||
let Some(repo) = self.active_repository.clone() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no active repo")));
|
||||
};
|
||||
repo.update(cx, |repo, cx| {
|
||||
let git_log = repo.log(skip, max_count);
|
||||
cx.spawn(async move |_, _| git_log.await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_element(
|
||||
&self,
|
||||
item_id: ElementId,
|
||||
commit: &CommitDetails,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let commit_summary = CommitSummary {
|
||||
sha: commit.sha.clone(),
|
||||
subject: commit
|
||||
.message
|
||||
.as_ref()
|
||||
.map_or(Default::default(), |message| {
|
||||
message
|
||||
.message
|
||||
.split('\n')
|
||||
.next()
|
||||
.unwrap()
|
||||
.trim_end()
|
||||
.to_string()
|
||||
.into()
|
||||
}),
|
||||
commit_timestamp: commit.commit_time.unix_timestamp(),
|
||||
has_parent: false,
|
||||
};
|
||||
|
||||
ListItem::new(item_id)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.h_8()
|
||||
.text_xs()
|
||||
.text_color(Color::Default.color(cx))
|
||||
.child(commit_summary.subject.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.h_8()
|
||||
.text_xs()
|
||||
.text_color(Color::Hidden.color(cx))
|
||||
.child(commit.author_name.clone())
|
||||
.ml_1(),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let workspace = self.workspace.clone();
|
||||
let repo = self.active_repository.as_ref().map(|repo| repo.downgrade());
|
||||
move |_, window, cx| {
|
||||
let repo = match repo.clone() {
|
||||
Some(repo) => repo,
|
||||
None => return,
|
||||
};
|
||||
CommitView::open(commit_summary.clone(), repo, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
|
||||
div()
|
||||
.occlude()
|
||||
.id("git-history-scrollbar")
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GitCommitList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
.flex_shrink()
|
||||
.h_48()
|
||||
.w_full()
|
||||
.child(
|
||||
list(
|
||||
self.commits_list.clone(),
|
||||
cx.processor(move |list, index, window, cx| {
|
||||
let item: &CommitDetails = &list.commits[index];
|
||||
|
||||
list.render_element(ElementId::Name(item.sha.clone()), item, window, cx)
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
.size_full(),
|
||||
)
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ use crate::askpass_modal::AskPassModal;
|
|||
use crate::commit_modal::CommitModal;
|
||||
use crate::commit_tooltip::CommitTooltip;
|
||||
use crate::commit_view::CommitView;
|
||||
use crate::git_commit_list::GitCommitList;
|
||||
use crate::git_panel_settings::StatusStyle;
|
||||
use crate::project_diff::{self, Diff, ProjectDiff};
|
||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||
|
@ -370,6 +371,7 @@ pub struct GitPanel {
|
|||
local_committer_task: Option<Task<()>>,
|
||||
bulk_staging: Option<BulkStaging>,
|
||||
_settings_subscription: Subscription,
|
||||
history: Entity<GitCommitList>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
@ -520,6 +522,8 @@ impl GitPanel {
|
|||
)
|
||||
.detach();
|
||||
|
||||
let history = GitCommitList::new(workspace, window, cx);
|
||||
|
||||
let mut this = Self {
|
||||
active_repository,
|
||||
commit_editor,
|
||||
|
@ -559,6 +563,7 @@ impl GitPanel {
|
|||
vertical_scrollbar,
|
||||
bulk_staging: None,
|
||||
_settings_subscription,
|
||||
history,
|
||||
};
|
||||
|
||||
this.schedule_update(false, window, cx);
|
||||
|
@ -4556,6 +4561,9 @@ impl Render for GitPanel {
|
|||
.when(!self.amend_pending, |this| {
|
||||
this.children(self.render_previous_commit(cx))
|
||||
})
|
||||
.when(GitPanelSettings::get_global(cx).commit_history, |this| {
|
||||
this.child(self.history.clone())
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||
|
|
|
@ -75,6 +75,11 @@ pub struct GitPanelSettingsContent {
|
|||
///
|
||||
/// Default: false
|
||||
pub collapse_untracked_diff: Option<bool>,
|
||||
|
||||
//// Whether to show a list of previous commits in the git panel.
|
||||
///
|
||||
/// Default: false
|
||||
pub commit_history: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq)]
|
||||
|
@ -87,6 +92,7 @@ pub struct GitPanelSettings {
|
|||
pub fallback_branch_name: String,
|
||||
pub sort_by_path: bool,
|
||||
pub collapse_untracked_diff: bool,
|
||||
pub commit_history: bool,
|
||||
}
|
||||
|
||||
impl Settings for GitPanelSettings {
|
||||
|
|
|
@ -29,6 +29,7 @@ pub mod commit_tooltip;
|
|||
mod commit_view;
|
||||
mod conflict_view;
|
||||
pub mod file_diff_view;
|
||||
pub mod git_commit_list;
|
||||
pub mod git_panel;
|
||||
mod git_panel_settings;
|
||||
pub mod onboarding;
|
||||
|
|
|
@ -3469,6 +3469,41 @@ impl Repository {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn log(
|
||||
&mut self,
|
||||
skip: u64,
|
||||
max_count: u64,
|
||||
) -> oneshot::Receiver<Result<Vec<CommitDetails>>> {
|
||||
let id = self.id;
|
||||
self.send_job(None, move |git_repo, _cx| async move {
|
||||
match git_repo {
|
||||
RepositoryState::Local { backend, .. } => backend.log(skip, max_count).await,
|
||||
RepositoryState::Remote { project_id, client } => {
|
||||
let resp = client
|
||||
.request(proto::GitLog {
|
||||
project_id: project_id.0,
|
||||
repository_id: id.to_proto(),
|
||||
skip,
|
||||
max_count,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(resp
|
||||
.commits
|
||||
.into_iter()
|
||||
.map(|commit| CommitDetails {
|
||||
sha: commit.sha.into(),
|
||||
message: commit.message.into(),
|
||||
commit_timestamp: commit.commit_timestamp,
|
||||
author_email: commit.author_email.into(),
|
||||
author_name: commit.author_name.into(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_commit_diff(&mut self, commit: String) -> oneshot::Receiver<Result<CommitDiff>> {
|
||||
let id = self.id;
|
||||
self.send_job(None, move |git_repo, cx| async move {
|
||||
|
|
|
@ -230,6 +230,18 @@ message GitShow {
|
|||
string commit = 4;
|
||||
}
|
||||
|
||||
message GitLog {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 repository_id = 3;
|
||||
uint64 skip = 4;
|
||||
uint64 max_count = 5;
|
||||
}
|
||||
|
||||
message GitLogResponse {
|
||||
repeated GitCommitDetails commits = 1;
|
||||
}
|
||||
|
||||
message GitCommitDetails {
|
||||
string sha = 1;
|
||||
string message = 2;
|
||||
|
|
|
@ -396,7 +396,10 @@ message Envelope {
|
|||
GitCloneResponse git_clone_response = 364;
|
||||
|
||||
LspQuery lsp_query = 365;
|
||||
LspQueryResponse lsp_query_response = 366; // current max
|
||||
LspQueryResponse lsp_query_response = 366;
|
||||
|
||||
GitLog git_log = 367;
|
||||
GitLogResponse git_log_response = 368; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
|
|
|
@ -284,6 +284,8 @@ messages!(
|
|||
(GitReset, Background),
|
||||
(GitCheckoutFiles, Background),
|
||||
(GitShow, Background),
|
||||
(GitLog, Background),
|
||||
(GitLogResponse, Background),
|
||||
(GitCommitDetails, Background),
|
||||
(SetIndexText, Background),
|
||||
(Push, Background),
|
||||
|
@ -462,6 +464,7 @@ request_messages!(
|
|||
(InstallExtension, Ack),
|
||||
(RegisterBufferWithLanguageServers, Ack),
|
||||
(GitShow, GitCommitDetails),
|
||||
(GitLog, GitLogResponse),
|
||||
(GitReset, Ack),
|
||||
(GitCheckoutFiles, Ack),
|
||||
(SetIndexText, Ack),
|
||||
|
@ -609,6 +612,7 @@ entity_messages!(
|
|||
CancelLanguageServerWork,
|
||||
RegisterBufferWithLanguageServers,
|
||||
GitShow,
|
||||
GitLog,
|
||||
GitReset,
|
||||
GitCheckoutFiles,
|
||||
SetIndexText,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue