diff --git a/Cargo.lock b/Cargo.lock index 038a102e1060df..eb44454c909377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5371,6 +5371,8 @@ dependencies = [ "serde_json", "settings", "theme", + "time", + "time_format", "ui", "util", "windows 0.58.0", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 450e435bb37d3c..6fa7475adcc810 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -631,7 +631,7 @@ } }, { - "context": "GitPanel || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome", + "context": "ChangesList || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome", "bindings": { ":": "command_palette::Toggle", "g /": "pane::DeploySearch" diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index ba6fdeb9290d38..30d36cfe8c1070 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -101,6 +101,7 @@ CREATE TABLE "worktree_repositories" ( "scan_id" INTEGER NOT NULL, "is_deleted" BOOL NOT NULL, "current_merge_conflicts" VARCHAR, + "branch_summary" VARCHAR, PRIMARY KEY(project_id, worktree_id, work_directory_id), FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE diff --git a/crates/collab/migrations/20250210223746_add_branch_summary.sql b/crates/collab/migrations/20250210223746_add_branch_summary.sql new file mode 100644 index 00000000000000..3294f38b94114a --- /dev/null +++ b/crates/collab/migrations/20250210223746_add_branch_summary.sql @@ -0,0 +1,2 @@ +ALTER TABLE worktree_repositories +ADD COLUMN worktree_repositories VARCHAR NULL; diff --git a/crates/collab/migrations/20250212060936_add_worktree_branch_summary.sql b/crates/collab/migrations/20250212060936_add_worktree_branch_summary.sql new file mode 100644 index 00000000000000..d7e3c04e2ff784 --- /dev/null +++ b/crates/collab/migrations/20250212060936_add_worktree_branch_summary.sql @@ -0,0 +1 @@ +ALTER TABLE worktree_repositories ADD COLUMN branch_summary TEXT NULL; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 2755f1223037b1..1cff5b53b09c12 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -326,16 +326,26 @@ impl Database { if !update.updated_repositories.is_empty() { worktree_repository::Entity::insert_many(update.updated_repositories.iter().map( - |repository| worktree_repository::ActiveModel { - project_id: ActiveValue::set(project_id), - worktree_id: ActiveValue::set(worktree_id), - work_directory_id: ActiveValue::set(repository.work_directory_id as i64), - scan_id: ActiveValue::set(update.scan_id as i64), - branch: ActiveValue::set(repository.branch.clone()), - is_deleted: ActiveValue::set(false), - current_merge_conflicts: ActiveValue::Set(Some( - serde_json::to_string(&repository.current_merge_conflicts).unwrap(), - )), + |repository| { + worktree_repository::ActiveModel { + project_id: ActiveValue::set(project_id), + worktree_id: ActiveValue::set(worktree_id), + work_directory_id: ActiveValue::set( + repository.work_directory_id as i64, + ), + scan_id: ActiveValue::set(update.scan_id as i64), + branch: ActiveValue::set(repository.branch.clone()), + is_deleted: ActiveValue::set(false), + branch_summary: ActiveValue::Set( + repository + .branch_summary + .as_ref() + .map(|summary| serde_json::to_string(summary).unwrap()), + ), + current_merge_conflicts: ActiveValue::Set(Some( + serde_json::to_string(&repository.current_merge_conflicts).unwrap(), + )), + } }, )) .on_conflict( @@ -347,6 +357,8 @@ impl Database { .update_columns([ worktree_repository::Column::ScanId, worktree_repository::Column::Branch, + worktree_repository::Column::BranchSummary, + worktree_repository::Column::CurrentMergeConflicts, ]) .to_owned(), ) @@ -779,6 +791,13 @@ impl Database { .transpose()? .unwrap_or_default(); + let branch_summary = db_repository_entry + .branch_summary + .as_ref() + .map(|branch_summary| serde_json::from_str(&branch_summary)) + .transpose()? + .unwrap_or_default(); + worktree.repository_entries.insert( db_repository_entry.work_directory_id as u64, proto::RepositoryEntry { @@ -787,6 +806,7 @@ impl Database { updated_statuses, removed_statuses: Vec::new(), current_merge_conflicts, + branch_summary, }, ); } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 8c9089dd756019..3f65cc4258e6c1 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -743,12 +743,20 @@ impl Database { .transpose()? .unwrap_or_default(); + let branch_summary = db_repository + .branch_summary + .as_ref() + .map(|branch_summary| serde_json::from_str(&branch_summary)) + .transpose()? + .unwrap_or_default(); + worktree.updated_repositories.push(proto::RepositoryEntry { work_directory_id: db_repository.work_directory_id as u64, branch: db_repository.branch, updated_statuses, removed_statuses, current_merge_conflicts, + branch_summary, }); } } diff --git a/crates/collab/src/db/tables/worktree_repository.rs b/crates/collab/src/db/tables/worktree_repository.rs index 66ff7b76430ef0..66247f9f17c45e 100644 --- a/crates/collab/src/db/tables/worktree_repository.rs +++ b/crates/collab/src/db/tables/worktree_repository.rs @@ -15,6 +15,8 @@ pub struct Model { pub is_deleted: bool, // JSON array typed string pub current_merge_conflicts: Option, + // A JSON object representing the current Branch values + pub branch_summary: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b511249f22c672..7e4f72007da38e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -395,6 +395,8 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_message_handler(broadcast_project_message_from_host::) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index be9890dcb84275..98d17f2b31490a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2895,7 +2895,10 @@ async fn test_git_branch_name( assert_eq!(worktrees.len(), 1); let worktree = worktrees[0].clone(); let root_entry = worktree.read(cx).snapshot().root_git_entry().unwrap(); - assert_eq!(root_entry.branch(), branch_name.map(Into::into)); + assert_eq!( + root_entry.branch().map(|branch| branch.name.to_string()), + branch_name + ); } // Smoke test branch reading @@ -6783,7 +6786,7 @@ async fn test_remote_git_branches( }) }); - assert_eq!(host_branch.as_ref(), branches[2]); + assert_eq!(host_branch.name, branches[2]); // Also try creating a new branch cx_b.update(|cx| { @@ -6804,5 +6807,5 @@ async fn test_remote_git_branches( }) }); - assert_eq!(host_branch.as_ref(), "totally-new-branch"); + assert_eq!(host_branch.name, "totally-new-branch"); } diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index c251204459b0ee..ec132a0c07c2fe 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -314,7 +314,7 @@ async fn test_ssh_collaboration_git_branches( }) }); - assert_eq!(server_branch.as_ref(), branches[2]); + assert_eq!(server_branch.name, branches[2]); // Also try creating a new branch cx_b.update(|cx| { @@ -337,7 +337,7 @@ async fn test_ssh_collaboration_git_branches( }) }); - assert_eq!(server_branch.as_ref(), "totally-new-branch"); + assert_eq!(server_branch.name, "totally-new-branch"); } #[gpui::test] diff --git a/crates/editor/src/blame_entry_tooltip.rs b/crates/editor/src/commit_tooltip.rs similarity index 73% rename from crates/editor/src/blame_entry_tooltip.rs rename to crates/editor/src/commit_tooltip.rs index 755f63cc4078da..f45b8d2b62da77 100644 --- a/crates/editor/src/blame_entry_tooltip.rs +++ b/crates/editor/src/commit_tooltip.rs @@ -1,28 +1,48 @@ use futures::Future; use git::blame::BlameEntry; -use git::Oid; +use git::PullRequest; use gpui::{ App, Asset, ClipboardItem, Element, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, WeakEntity, }; +use language::ParsedMarkdown; use settings::Settings; use std::hash::Hash; use theme::ThemeSettings; -use time::UtcOffset; +use time::{OffsetDateTime, UtcOffset}; +use time_format::format_local_timestamp; use ui::{prelude::*, tooltip_container, Avatar, Divider, IconButtonShape}; +use url::Url; use workspace::Workspace; -use crate::git::blame::{CommitDetails, GitRemote}; +use crate::git::blame::GitRemote; use crate::EditorStyle; +#[derive(Clone, Debug)] +pub struct CommitDetails { + pub sha: SharedString, + pub committer_name: SharedString, + pub committer_email: SharedString, + pub commit_time: OffsetDateTime, + pub message: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct ParsedCommitMessage { + pub message: SharedString, + pub parsed_message: ParsedMarkdown, + pub permalink: Option, + pub pull_request: Option, + pub remote: Option, +} + struct CommitAvatar<'a> { - details: Option<&'a CommitDetails>, - sha: Oid, + commit: &'a CommitDetails, } impl<'a> CommitAvatar<'a> { - fn new(details: Option<&'a CommitDetails>, sha: Oid) -> Self { - Self { details, sha } + fn new(details: &'a CommitDetails) -> Self { + Self { commit: details } } } @@ -30,14 +50,16 @@ impl<'a> CommitAvatar<'a> { fn render( &'a self, window: &mut Window, - cx: &mut Context, + cx: &mut Context, ) -> Option { let remote = self - .details + .commit + .message + .as_ref() .and_then(|details| details.remote.as_ref()) .filter(|remote| remote.host_supports_avatars())?; - let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha); + let avatar_url = CommitAvatarAsset::new(remote.clone(), self.commit.sha.clone()); let element = match window.use_asset::(&avatar_url, cx) { // Loading or no avatar found @@ -54,7 +76,7 @@ impl<'a> CommitAvatar<'a> { #[derive(Clone, Debug)] struct CommitAvatarAsset { - sha: Oid, + sha: SharedString, remote: GitRemote, } @@ -66,7 +88,7 @@ impl Hash for CommitAvatarAsset { } impl CommitAvatarAsset { - fn new(remote: GitRemote, sha: Oid) -> Self { + fn new(remote: GitRemote, sha: SharedString) -> Self { Self { remote, sha } } } @@ -91,50 +113,78 @@ impl Asset for CommitAvatarAsset { } } -pub(crate) struct BlameEntryTooltip { - blame_entry: BlameEntry, - details: Option, +pub struct CommitTooltip { + commit: CommitDetails, editor_style: EditorStyle, workspace: Option>, scroll_handle: ScrollHandle, } -impl BlameEntryTooltip { - pub(crate) fn new( - blame_entry: BlameEntry, - details: Option, - style: &EditorStyle, +impl CommitTooltip { + pub fn blame_entry( + blame: BlameEntry, + details: Option, + style: EditorStyle, + workspace: Option>, + ) -> Self { + let commit_time = blame + .committer_time + .and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()) + .unwrap_or(OffsetDateTime::now_utc()); + Self::new( + CommitDetails { + sha: blame.sha.to_string().into(), + commit_time, + committer_name: blame + .committer_name + .unwrap_or("".to_string()) + .into(), + committer_email: blame.committer_email.unwrap_or("".to_string()).into(), + message: details, + }, + style, + workspace, + ) + } + + pub fn new( + commit: CommitDetails, + editor_style: EditorStyle, workspace: Option>, ) -> Self { Self { - editor_style: style.clone(), - blame_entry, - details, + editor_style, + commit, workspace, scroll_handle: ScrollHandle::new(), } } } -impl Render for BlameEntryTooltip { +impl Render for CommitTooltip { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let avatar = - CommitAvatar::new(self.details.as_ref(), self.blame_entry.sha).render(window, cx); + let avatar = CommitAvatar::new(&self.commit).render(window, cx); - let author = self - .blame_entry - .author - .clone() - .unwrap_or("".to_string()); + let author = self.commit.committer_name.clone(); - let author_email = self.blame_entry.author_mail.clone(); + let author_email = self.commit.committer_email.clone(); - let short_commit_id = self.blame_entry.sha.display_short(); - let full_sha = self.blame_entry.sha.to_string().clone(); - let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry); + let short_commit_id = self + .commit + .sha + .get(0..8) + .map(|sha| sha.to_string().into()) + .unwrap_or_else(|| self.commit.sha.clone()); + let full_sha = self.commit.sha.to_string().clone(); + let absolute_timestamp = format_local_timestamp( + self.commit.commit_time, + OffsetDateTime::now_utc(), + time_format::TimestampFormat::MediumAbsolute, + ); let message = self - .details + .commit + .message .as_ref() .map(|details| { crate::render_parsed_markdown( @@ -149,7 +199,8 @@ impl Render for BlameEntryTooltip { .unwrap_or("".into_any()); let pull_request = self - .details + .commit + .message .as_ref() .and_then(|details| details.pull_request.clone()); @@ -171,7 +222,7 @@ impl Render for BlameEntryTooltip { .flex_wrap() .children(avatar) .child(author) - .when_some(author_email, |this, author_email| { + .when(!author_email.is_empty(), |this| { this.child( div() .text_color(cx.theme().colors().text_muted) @@ -231,12 +282,16 @@ impl Render for BlameEntryTooltip { .icon_color(Color::Muted) .icon_position(IconPosition::Start) .disabled( - self.details.as_ref().map_or(true, |details| { - details.permalink.is_none() - }), + self.commit + .message + .as_ref() + .map_or(true, |details| { + details.permalink.is_none() + }), ) .when_some( - self.details + self.commit + .message .as_ref() .and_then(|details| details.permalink.clone()), |this, url| { @@ -284,7 +339,3 @@ fn blame_entry_timestamp(blame_entry: &BlameEntry, format: time_format::Timestam pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String { blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative) } - -fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry) -> String { - blame_entry_timestamp(blame_entry, time_format::TimestampFormat::MediumAbsolute) -} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 66b76991a6b2d8..f77a32a92bbcff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13,10 +13,10 @@ //! //! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior. pub mod actions; -mod blame_entry_tooltip; mod blink_manager; mod clangd_ext; mod code_context_menus; +pub mod commit_tooltip; pub mod display_map; mod editor_settings; mod editor_settings_controls; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4cea82664b9f25..c6e55f483b352e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,6 +1,6 @@ use crate::{ - blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip}, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, + commit_tooltip::{blame_entry_relative_timestamp, CommitTooltip, ParsedCommitMessage}, display_map::{ Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint, }, @@ -8,7 +8,7 @@ use crate::{ CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine, ScrollbarDiagnostics, ShowScrollbar, }, - git::blame::{CommitDetails, GitBlame}, + git::blame::GitBlame, hover_popover::{ self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, @@ -5939,7 +5939,8 @@ fn render_inline_blame_entry( let details = blame.read(cx).details_for_entry(&blame_entry); - let tooltip = cx.new(|_| BlameEntryTooltip::new(blame_entry, details, style, workspace)); + let tooltip = + cx.new(|_| CommitTooltip::blame_entry(blame_entry, details, style.clone(), workspace)); h_flex() .id("inline-blame") @@ -5989,8 +5990,14 @@ fn render_blame_entry( let workspace = editor.read(cx).workspace.as_ref().map(|(w, _)| w.clone()); - let tooltip = - cx.new(|_| BlameEntryTooltip::new(blame_entry.clone(), details.clone(), style, workspace)); + let tooltip = cx.new(|_| { + CommitTooltip::blame_entry( + blame_entry.clone(), + details.clone(), + style.clone(), + workspace, + ) + }); h_flex() .w_full() @@ -6040,7 +6047,7 @@ fn render_blame_entry( fn deploy_blame_entry_context_menu( blame_entry: &BlameEntry, - details: Option<&CommitDetails>, + details: Option<&ParsedCommitMessage>, editor: Entity, position: gpui::Point, window: &mut Window, diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 767c1eabb9e0e0..d8ff8c359fc024 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -2,7 +2,7 @@ use anyhow::Result; use collections::HashMap; use git::{ blame::{Blame, BlameEntry}, - parse_git_remote_url, GitHostingProvider, GitHostingProviderRegistry, Oid, PullRequest, + parse_git_remote_url, GitHostingProvider, GitHostingProviderRegistry, Oid, }; use gpui::{App, Context, Entity, Subscription, Task}; use http_client::HttpClient; @@ -12,8 +12,11 @@ use project::{Project, ProjectItem}; use smallvec::SmallVec; use std::{sync::Arc, time::Duration}; use sum_tree::SumTree; +use ui::SharedString; use url::Url; +use crate::commit_tooltip::ParsedCommitMessage; + #[derive(Clone, Debug, Default)] pub struct GitBlameEntry { pub rows: u32, @@ -77,7 +80,11 @@ impl GitRemote { self.host.supports_avatars() } - pub async fn avatar_url(&self, commit: Oid, client: Arc) -> Option { + pub async fn avatar_url( + &self, + commit: SharedString, + client: Arc, + ) -> Option { self.host .commit_author_avatar_url(&self.owner, &self.repo, commit, client) .await @@ -85,21 +92,11 @@ impl GitRemote { .flatten() } } - -#[derive(Clone, Debug)] -pub struct CommitDetails { - pub message: String, - pub parsed_message: ParsedMarkdown, - pub permalink: Option, - pub pull_request: Option, - pub remote: Option, -} - pub struct GitBlame { project: Entity, buffer: Entity, entries: SumTree, - commit_details: HashMap, + commit_details: HashMap, buffer_snapshot: BufferSnapshot, buffer_edits: text::Subscription, task: Task>, @@ -187,7 +184,7 @@ impl GitBlame { self.generated } - pub fn details_for_entry(&self, entry: &BlameEntry) -> Option { + pub fn details_for_entry(&self, entry: &BlameEntry) -> Option { self.commit_details.get(&entry.sha).cloned() } @@ -480,7 +477,7 @@ async fn parse_commit_messages( deprecated_permalinks: &HashMap, provider_registry: Arc, languages: &Arc, -) -> HashMap { +) -> HashMap { let mut commit_details = HashMap::default(); let parsed_remote_url = remote_url @@ -519,8 +516,8 @@ async fn parse_commit_messages( commit_details.insert( oid, - CommitDetails { - message, + ParsedCommitMessage { + message: message.into(), parsed_message, permalink, remote, diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index e4947e5bbd6dae..fd87fcb7aa1b63 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -132,8 +132,8 @@ pub struct BlameEntry { pub author_time: Option, pub author_tz: Option, - pub committer: Option, - pub committer_mail: Option, + pub committer_name: Option, + pub committer_email: Option, pub committer_time: Option, pub committer_tz: Option, @@ -255,10 +255,12 @@ fn parse_git_blame(output: &str) -> Result> { .clone_from(&existing_entry.author_mail); new_entry.author_time = existing_entry.author_time; new_entry.author_tz.clone_from(&existing_entry.author_tz); - new_entry.committer.clone_from(&existing_entry.committer); new_entry - .committer_mail - .clone_from(&existing_entry.committer_mail); + .committer_name + .clone_from(&existing_entry.committer_name); + new_entry + .committer_email + .clone_from(&existing_entry.committer_email); new_entry.committer_time = existing_entry.committer_time; new_entry .committer_tz @@ -288,8 +290,8 @@ fn parse_git_blame(output: &str) -> Result> { } "author-tz" if is_committed => entry.author_tz = Some(value.into()), - "committer" if is_committed => entry.committer = Some(value.into()), - "committer-mail" if is_committed => entry.committer_mail = Some(value.into()), + "committer" if is_committed => entry.committer_name = Some(value.into()), + "committer-mail" if is_committed => entry.committer_email = Some(value.into()), "committer-time" if is_committed => { entry.committer_time = Some(value.parse::()?) } diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 42da2e917083d1..b9b67d84153811 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -38,6 +38,7 @@ actions!( StageAll, UnstageAll, RevertAll, + Uncommit, Commit, ClearCommitMessage ] diff --git a/crates/git/src/hosting_provider.rs b/crates/git/src/hosting_provider.rs index 94069bd9e80d2d..2b875418bf34bc 100644 --- a/crates/git/src/hosting_provider.rs +++ b/crates/git/src/hosting_provider.rs @@ -4,13 +4,11 @@ use anyhow::Result; use async_trait::async_trait; use collections::BTreeMap; use derive_more::{Deref, DerefMut}; -use gpui::{App, Global}; +use gpui::{App, Global, SharedString}; use http_client::HttpClient; use parking_lot::RwLock; use url::Url; -use crate::Oid; - #[derive(Debug, PartialEq, Eq, Clone)] pub struct PullRequest { pub number: u32, @@ -83,7 +81,7 @@ pub trait GitHostingProvider { &self, _repo_owner: &str, _repo: &str, - _commit: Oid, + _commit: SharedString, _http_client: Arc, ) -> Result> { Ok(None) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 24f8689d0445d1..ffc7450858a2a4 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1,7 +1,7 @@ use crate::status::FileStatus; use crate::GitHostingProviderRegistry; use crate::{blame::Blame, status::GitStatus}; -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{anyhow, Context, Result}; use collections::{HashMap, HashSet}; use git2::BranchType; use gpui::SharedString; @@ -20,12 +20,63 @@ use sum_tree::MapSeekTarget; use util::command::new_std_command; use util::ResultExt; -#[derive(Clone, Debug, Hash, PartialEq)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Branch { pub is_head: bool, pub name: SharedString, - /// Timestamp of most recent commit, normalized to Unix Epoch format. - pub unix_timestamp: Option, + pub upstream: Option, + pub most_recent_commit: Option, +} + +impl Branch { + pub fn priority_key(&self) -> (bool, Option) { + ( + self.is_head, + self.most_recent_commit + .as_ref() + .map(|commit| commit.commit_timestamp), + ) + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct Upstream { + pub ref_name: SharedString, + pub tracking: Option, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct UpstreamTracking { + pub ahead: u32, + pub behind: u32, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct CommitSummary { + pub sha: SharedString, + pub subject: SharedString, + /// This is a unix timestamp + pub commit_timestamp: i64, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct CommitDetails { + pub sha: SharedString, + pub message: SharedString, + pub commit_timestamp: i64, + pub committer_email: SharedString, + pub committer_name: SharedString, +} + +pub enum ResetMode { + // reset the branch pointer, leave index and worktree unchanged + // (this will make it look like things that were committed are now + // staged) + Soft, + // reset the branch pointer and index, leave worktree unchanged + // (this makes it look as though things that were committed are now + // unstaged) + Mixed, } pub trait GitRepository: Send + Sync { @@ -45,7 +96,6 @@ pub trait GitRepository: Send + Sync { /// Returns the URL of the remote with the given name. fn remote_url(&self, name: &str) -> Option; - fn branch_name(&self) -> Option; /// Returns the SHA of the current HEAD. fn head_sha(&self) -> Option; @@ -60,6 +110,10 @@ pub trait GitRepository: Send + Sync { fn create_branch(&self, _: &str) -> Result<()>; fn branch_exits(&self, _: &str) -> Result; + fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>; + + fn show(&self, commit: &str) -> Result; + fn blame(&self, path: &Path, content: Rope) -> Result; /// Returns the absolute path to the repository. For worktrees, this will be the path to the @@ -132,6 +186,53 @@ impl GitRepository for RealGitRepository { repo.commondir().into() } + fn show(&self, commit: &str) -> Result { + let repo = self.repository.lock(); + let Ok(commit) = repo.revparse_single(commit)?.into_commit() else { + anyhow::bail!("{} is not a commit", commit); + }; + let details = CommitDetails { + sha: commit.id().to_string().into(), + message: String::from_utf8_lossy(commit.message_raw_bytes()) + .to_string() + .into(), + commit_timestamp: commit.time().seconds(), + committer_email: String::from_utf8_lossy(commit.committer().email_bytes()) + .to_string() + .into(), + committer_name: String::from_utf8_lossy(commit.committer().name_bytes()) + .to_string() + .into(), + }; + Ok(details) + } + + fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> { + let working_directory = self + .repository + .lock() + .workdir() + .context("failed to read git work directory")? + .to_path_buf(); + + let mode_flag = match mode { + ResetMode::Mixed => "--mixed", + ResetMode::Soft => "--soft", + }; + + let output = new_std_command(&self.git_binary_path) + .current_dir(&working_directory) + .args(["reset", mode_flag, commit]) + .output()?; + if !output.status.success() { + return Err(anyhow!( + "Failed to reset:\n{}", + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(()) + } + fn load_index_text(&self, path: &RepoPath) -> Option { fn logic(repo: &git2::Repository, path: &RepoPath) -> Result> { const STAGE_NORMAL: i32 = 0; @@ -215,13 +316,6 @@ impl GitRepository for RealGitRepository { remote.url().map(|url| url.to_string()) } - fn branch_name(&self) -> Option { - let repo = self.repository.lock(); - let head = repo.head().log_err()?; - let branch = String::from_utf8_lossy(head.shorthand_bytes()); - Some(branch.to_string()) - } - fn head_sha(&self) -> Option { Some(self.repository.lock().head().ok()?.target()?.to_string()) } @@ -261,33 +355,62 @@ impl GitRepository for RealGitRepository { } fn branches(&self) -> Result> { - let repo = self.repository.lock(); - let local_branches = repo.branches(Some(BranchType::Local))?; - let valid_branches = local_branches - .filter_map(|branch| { - branch.ok().and_then(|(branch, _)| { - let is_head = branch.is_head(); - let name = branch - .name() - .ok() - .flatten() - .map(|name| name.to_string().into())?; - let timestamp = branch.get().peel_to_commit().ok()?.time(); - let unix_timestamp = timestamp.seconds(); - let timezone_offset = timestamp.offset_minutes(); - let utc_offset = - time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?; - let unix_timestamp = - time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?; - Some(Branch { - is_head, - name, - unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()), - }) - }) - }) - .collect(); - Ok(valid_branches) + let working_directory = self + .repository + .lock() + .workdir() + .context("failed to read git work directory")? + .to_path_buf(); + let fields = [ + "%(HEAD)", + "%(objectname)", + "%(refname)", + "%(upstream)", + "%(upstream:track)", + "%(committerdate:unix)", + "%(contents:subject)", + ] + .join("%00"); + let args = vec!["for-each-ref", "refs/heads/*", "--format", &fields]; + + let output = new_std_command(&self.git_binary_path) + .current_dir(&working_directory) + .args(args) + .output()?; + + if !output.status.success() { + return Err(anyhow!( + "Failed to git git branches:\n{}", + String::from_utf8_lossy(&output.stderr) + )); + } + + let input = String::from_utf8_lossy(&output.stdout); + + let mut branches = parse_branch_input(&input)?; + if branches.is_empty() { + let args = vec!["symbolic-ref", "--quiet", "--short", "HEAD"]; + + let output = new_std_command(&self.git_binary_path) + .current_dir(&working_directory) + .args(args) + .output()?; + + // git symbolic-ref returns a non-0 exit code if HEAD points + // to something other than a branch + if output.status.success() { + let name = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + branches.push(Branch { + name: name.into(), + is_head: true, + upstream: None, + most_recent_commit: None, + }); + } + } + + Ok(branches) } fn change_branch(&self, name: &str) -> Result<()> { @@ -478,11 +601,6 @@ impl GitRepository for FakeGitRepository { None } - fn branch_name(&self) -> Option { - let state = self.state.lock(); - state.current_branch_name.clone() - } - fn head_sha(&self) -> Option { None } @@ -491,6 +609,14 @@ impl GitRepository for FakeGitRepository { vec![] } + fn show(&self, _: &str) -> Result { + unimplemented!() + } + + fn reset(&self, _: &str, _: ResetMode) -> Result<()> { + unimplemented!() + } + fn path(&self) -> PathBuf { let state = self.state.lock(); state.path.clone() @@ -533,7 +659,8 @@ impl GitRepository for FakeGitRepository { .map(|branch_name| Branch { is_head: Some(branch_name) == current_branch.as_ref(), name: branch_name.into(), - unix_timestamp: None, + most_recent_commit: None, + upstream: None, }) .collect()) } @@ -703,3 +830,106 @@ impl<'a> MapSeekTarget for RepoPathDescendants<'a> { } } } + +fn parse_branch_input(input: &str) -> Result> { + let mut branches = Vec::new(); + for line in input.split('\n') { + if line.is_empty() { + continue; + } + let mut fields = line.split('\x00'); + let is_current_branch = fields.next().context("no HEAD")? == "*"; + let head_sha: SharedString = fields.next().context("no objectname")?.to_string().into(); + let ref_name: SharedString = fields + .next() + .context("no refname")? + .strip_prefix("refs/heads/") + .context("unexpected format for refname")? + .to_string() + .into(); + let upstream_name = fields.next().context("no upstream")?.to_string(); + let upstream_tracking = parse_upstream_track(fields.next().context("no upstream:track")?)?; + let commiterdate = fields.next().context("no committerdate")?.parse::()?; + let subject: SharedString = fields + .next() + .context("no contents:subject")? + .to_string() + .into(); + + branches.push(Branch { + is_head: is_current_branch, + name: ref_name, + most_recent_commit: Some(CommitSummary { + sha: head_sha, + subject, + commit_timestamp: commiterdate, + }), + upstream: if upstream_name.is_empty() { + None + } else { + Some(Upstream { + ref_name: upstream_name.into(), + tracking: upstream_tracking, + }) + }, + }) + } + + Ok(branches) +} + +fn parse_upstream_track(upstream_track: &str) -> Result> { + if upstream_track == "" { + return Ok(Some(UpstreamTracking { + ahead: 0, + behind: 0, + })); + } + + let upstream_track = upstream_track + .strip_prefix("[") + .ok_or_else(|| anyhow!("missing ["))?; + let upstream_track = upstream_track + .strip_suffix("]") + .ok_or_else(|| anyhow!("missing ["))?; + let mut ahead: u32 = 0; + let mut behind: u32 = 0; + for component in upstream_track.split(", ") { + if component == "gone" { + return Ok(None); + } + if let Some(ahead_num) = component.strip_prefix("ahead ") { + ahead = ahead_num.parse::()?; + } + if let Some(behind_num) = component.strip_prefix("behind ") { + behind = behind_num.parse::()?; + } + } + Ok(Some(UpstreamTracking { ahead, behind })) +} + +#[test] +fn test_branches_parsing() { + // suppress "help: octal escapes are not supported, `\0` is always null" + #[allow(clippy::octal_escapes)] + let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0generated protobuf\n"; + assert_eq!( + parse_branch_input(&input).unwrap(), + vec![Branch { + is_head: true, + name: "zed-patches".into(), + upstream: Some(Upstream { + ref_name: "refs/remotes/origin/zed-patches".into(), + tracking: Some(UpstreamTracking { + ahead: 0, + behind: 0 + }) + }), + most_recent_commit: Some(CommitSummary { + sha: "060964da10574cd9bf06463a53bf6e0769c5c45e".into(), + subject: "generated protobuf".into(), + commit_timestamp: 1733187470, + }) + }] + ) +} diff --git a/crates/git/test_data/golden/blame_incremental_complex.json b/crates/git/test_data/golden/blame_incremental_complex.json index 3eb6ec81e33a5f..1f05fea8f5ec78 100644 --- a/crates/git/test_data/golden/blame_incremental_complex.json +++ b/crates/git/test_data/golden/blame_incremental_complex.json @@ -10,8 +10,8 @@ "author_mail": "<64036912+mmkaram@users.noreply.github.com>", "author_time": 1708621949, "author_tz": "-0800", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1708621949, "committer_tz": "-0700", "summary": "Add option to either use system clipboard or vim clipboard (#7936)", @@ -29,8 +29,8 @@ "author_mail": "<64036912+mmkaram@users.noreply.github.com>", "author_time": 1708621949, "author_tz": "-0800", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1708621949, "committer_tz": "-0700", "summary": "Add option to either use system clipboard or vim clipboard (#7936)", @@ -48,8 +48,8 @@ "author_mail": "<64036912+mmkaram@users.noreply.github.com>", "author_time": 1708621949, "author_tz": "-0800", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1708621949, "committer_tz": "-0700", "summary": "Add option to either use system clipboard or vim clipboard (#7936)", @@ -67,8 +67,8 @@ "author_mail": "<64036912+mmkaram@users.noreply.github.com>", "author_time": 1708621949, "author_tz": "-0800", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1708621949, "committer_tz": "-0700", "summary": "Add option to either use system clipboard or vim clipboard (#7936)", @@ -86,8 +86,8 @@ "author_mail": "<64036912+mmkaram@users.noreply.github.com>", "author_time": 1708621949, "author_tz": "-0800", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1708621949, "committer_tz": "-0700", "summary": "Add option to either use system clipboard or vim clipboard (#7936)", @@ -105,8 +105,8 @@ "author_mail": "<64036912+mmkaram@users.noreply.github.com>", "author_time": 1708621949, "author_tz": "-0800", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1708621949, "committer_tz": "-0700", "summary": "Add option to either use system clipboard or vim clipboard (#7936)", @@ -124,8 +124,8 @@ "author_mail": "<64036912+mmkaram@users.noreply.github.com>", "author_time": 1708621949, "author_tz": "-0800", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1708621949, "committer_tz": "-0700", "summary": "Add option to either use system clipboard or vim clipboard (#7936)", @@ -143,8 +143,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -162,8 +162,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -181,8 +181,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -200,8 +200,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -219,8 +219,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -238,8 +238,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -257,8 +257,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -276,8 +276,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -295,8 +295,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -314,8 +314,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -333,8 +333,8 @@ "author_mail": "", "author_time": 1707520689, "author_tz": "-0700", - "committer": "GitHub", - "committer_mail": "", + "committer_name": "GitHub", + "committer_email": "", "committer_time": 1707520689, "committer_tz": "-0700", "summary": "Highlight selections on vim yank (#7638)", @@ -352,8 +352,8 @@ "author_mail": "", "author_time": 1705619094, "author_tz": "-0800", - "committer": "Max Brunsfeld", - "committer_mail": "", + "committer_name": "Max Brunsfeld", + "committer_email": "", "committer_time": 1705619205, "committer_tz": "-0800", "summary": "Merge branch 'main' into language-api-docs", @@ -371,8 +371,8 @@ "author_mail": "", "author_time": 1705619094, "author_tz": "-0800", - "committer": "Max Brunsfeld", - "committer_mail": "", + "committer_name": "Max Brunsfeld", + "committer_email": "", "committer_time": 1705619205, "committer_tz": "-0800", "summary": "Merge branch 'main' into language-api-docs", @@ -390,8 +390,8 @@ "author_mail": "", "author_time": 1694798044, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1694798044, "committer_tz": "-0600", "summary": "Fix Y on last line with no trailing new line", @@ -409,8 +409,8 @@ "author_mail": "", "author_time": 1694798044, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1694798044, "committer_tz": "-0600", "summary": "Fix Y on last line with no trailing new line", @@ -428,8 +428,8 @@ "author_mail": "", "author_time": 1692855942, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1692856812, "committer_tz": "-0600", "summary": "vim: Fix linewise copy of last line with no trailing newline", @@ -447,8 +447,8 @@ "author_mail": "", "author_time": 1692855942, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1692856812, "committer_tz": "-0600", "summary": "vim: Fix linewise copy of last line with no trailing newline", @@ -466,8 +466,8 @@ "author_mail": "", "author_time": 1692855942, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1692856812, "committer_tz": "-0600", "summary": "vim: Fix linewise copy of last line with no trailing newline", @@ -485,8 +485,8 @@ "author_mail": "", "author_time": 1692855942, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1692856812, "committer_tz": "-0600", "summary": "vim: Fix linewise copy of last line with no trailing newline", @@ -504,8 +504,8 @@ "author_mail": "", "author_time": 1692855942, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1692856812, "committer_tz": "-0600", "summary": "vim: Fix linewise copy of last line with no trailing newline", @@ -523,8 +523,8 @@ "author_mail": "", "author_time": 1692644159, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1692732477, "committer_tz": "-0600", "summary": "Rewrite paste", @@ -542,8 +542,8 @@ "author_mail": "", "author_time": 1692644159, "author_tz": "-0600", - "committer": "Conrad Irwin", - "committer_mail": "", + "committer_name": "Conrad Irwin", + "committer_email": "", "committer_time": 1692732477, "committer_tz": "-0600", "summary": "Rewrite paste", @@ -561,8 +561,8 @@ "author_mail": "", "author_time": 1659072896, "author_tz": "-0700", - "committer": "Max Brunsfeld", - "committer_mail": "", + "committer_name": "Max Brunsfeld", + "committer_email": "", "committer_time": 1659073230, "committer_tz": "-0700", "summary": ":art: Rename and simplify some autoindent stuff", @@ -580,8 +580,8 @@ "author_mail": "", "author_time": 1653424557, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Unify visual line_mode and non line_mode operators", @@ -599,8 +599,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -618,8 +618,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -637,8 +637,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -656,8 +656,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -675,8 +675,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -694,8 +694,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -713,8 +713,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -732,8 +732,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -751,8 +751,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", @@ -770,8 +770,8 @@ "author_mail": "", "author_time": 1653007350, "author_tz": "-0700", - "committer": "Kaylee Simmons", - "committer_mail": "", + "committer_name": "Kaylee Simmons", + "committer_email": "", "committer_time": 1653609725, "committer_tz": "-0700", "summary": "Enable copy and paste in vim mode", diff --git a/crates/git/test_data/golden/blame_incremental_not_committed.json b/crates/git/test_data/golden/blame_incremental_not_committed.json index 4e4834d45c5f19..b50c793ad93947 100644 --- a/crates/git/test_data/golden/blame_incremental_not_committed.json +++ b/crates/git/test_data/golden/blame_incremental_not_committed.json @@ -10,8 +10,8 @@ "author_mail": "", "author_time": 1710764113, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1710764113, "committer_tz": "+0100", "summary": "Another commit", @@ -29,8 +29,8 @@ "author_mail": "", "author_time": 1710764113, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1710764113, "committer_tz": "+0100", "summary": "Another commit", @@ -48,8 +48,8 @@ "author_mail": "", "author_time": 1710764087, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1710764087, "committer_tz": "+0100", "summary": "Another commit", @@ -67,8 +67,8 @@ "author_mail": "", "author_time": 1710764087, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1710764087, "committer_tz": "+0100", "summary": "Another commit", @@ -86,8 +86,8 @@ "author_mail": "", "author_time": 1709299737, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1709299737, "committer_tz": "+0100", "summary": "Initial", @@ -105,8 +105,8 @@ "author_mail": "", "author_time": 1709299737, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1709299737, "committer_tz": "+0100", "summary": "Initial", @@ -124,8 +124,8 @@ "author_mail": "", "author_time": 1709299737, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1709299737, "committer_tz": "+0100", "summary": "Initial", diff --git a/crates/git/test_data/golden/blame_incremental_simple.json b/crates/git/test_data/golden/blame_incremental_simple.json index c8fba838972ac6..6e46ca3b692fe7 100644 --- a/crates/git/test_data/golden/blame_incremental_simple.json +++ b/crates/git/test_data/golden/blame_incremental_simple.json @@ -10,8 +10,8 @@ "author_mail": "", "author_time": 1709808710, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1709808710, "committer_tz": "+0100", "summary": "Make a commit", @@ -29,8 +29,8 @@ "author_mail": "", "author_time": 1709741400, "author_tz": "+0100", - "committer": "Joe Schmoe", - "committer_mail": "", + "committer_name": "Joe Schmoe", + "committer_email": "", "committer_time": 1709741400, "committer_tz": "+0100", "summary": "Joe's cool commit", @@ -48,8 +48,8 @@ "author_mail": "", "author_time": 1709741400, "author_tz": "+0100", - "committer": "Joe Schmoe", - "committer_mail": "", + "committer_name": "Joe Schmoe", + "committer_email": "", "committer_time": 1709741400, "committer_tz": "+0100", "summary": "Joe's cool commit", @@ -67,8 +67,8 @@ "author_mail": "", "author_time": 1709741400, "author_tz": "+0100", - "committer": "Joe Schmoe", - "committer_mail": "", + "committer_name": "Joe Schmoe", + "committer_email": "", "committer_time": 1709741400, "committer_tz": "+0100", "summary": "Joe's cool commit", @@ -86,8 +86,8 @@ "author_mail": "", "author_time": 1709129122, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1709129122, "committer_tz": "+0100", "summary": "Get to a state where eslint would change code and imports", @@ -105,8 +105,8 @@ "author_mail": "", "author_time": 1709128963, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1709128963, "committer_tz": "+0100", "summary": "Add some stuff", @@ -124,8 +124,8 @@ "author_mail": "", "author_time": 1709128963, "author_tz": "+0100", - "committer": "Thorsten Ball", - "committer_mail": "", + "committer_name": "Thorsten Ball", + "committer_email": "", "committer_time": 1709128963, "committer_tz": "+0100", "summary": "Add some stuff", diff --git a/crates/git_hosting_providers/src/providers/codeberg.rs b/crates/git_hosting_providers/src/providers/codeberg.rs index cb917823c5dbbf..0e01331278a6f2 100644 --- a/crates/git_hosting_providers/src/providers/codeberg.rs +++ b/crates/git_hosting_providers/src/providers/codeberg.rs @@ -4,12 +4,13 @@ use std::sync::Arc; use anyhow::{bail, Context, Result}; use async_trait::async_trait; use futures::AsyncReadExt; +use gpui::SharedString; use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request}; use serde::Deserialize; use url::Url; use git::{ - BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote, + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, RemoteUrl, }; @@ -160,7 +161,7 @@ impl GitHostingProvider for Codeberg { &self, repo_owner: &str, repo: &str, - commit: Oid, + commit: SharedString, http_client: Arc, ) -> Result> { let commit = commit.to_string(); diff --git a/crates/git_hosting_providers/src/providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs index 6026c6ed208f96..f86b586ea8c701 100644 --- a/crates/git_hosting_providers/src/providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -4,13 +4,14 @@ use std::sync::{Arc, LazyLock}; use anyhow::{bail, Context, Result}; use async_trait::async_trait; use futures::AsyncReadExt; +use gpui::SharedString; use http_client::{AsyncBody, HttpClient, HttpRequestExt, Request}; use regex::Regex; use serde::Deserialize; use url::Url; use git::{ - BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, Oid, ParsedGitRemote, + BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, PullRequest, RemoteUrl, }; @@ -178,7 +179,7 @@ impl GitHostingProvider for Github { &self, repo_owner: &str, repo: &str, - commit: Oid, + commit: SharedString, http_client: Arc, ) -> Result> { let commit = commit.to_string(); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 4f10e067b8d986..19e443766a1429 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -36,6 +36,8 @@ serde_derive.workspace = true serde_json.workspace = true settings.workspace = true theme.workspace = true +time.workspace = true +time_format.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index bff1c8bf52431c..d6233dd8237119 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -190,9 +190,7 @@ impl PickerDelegate for BranchListDelegate { // Truncate list of recent branches // Do a partial sort to show recent-ish branches first. branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| { - rhs.is_head - .cmp(&lhs.is_head) - .then(rhs.unix_timestamp.cmp(&lhs.unix_timestamp)) + rhs.priority_key().cmp(&lhs.priority_key()) }); branches.truncate(RECENT_BRANCHES_COUNT); } @@ -255,6 +253,25 @@ impl PickerDelegate for BranchListDelegate { let Some(branch) = self.matches.get(self.selected_index()) else { return; }; + + let current_branch = self + .workspace + .update(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .active_repository(cx) + .and_then(|repo| repo.read(cx).branch()) + .map(|branch| branch.name.to_string()) + }) + .ok() + .flatten(); + + if current_branch == Some(branch.name().to_string()) { + cx.emit(DismissEvent); + return; + } + cx.spawn_in(window, { let branch = branch.clone(); |picker, mut cx| async move { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index b3675f249438fe..cf7d77754e5628 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -6,13 +6,15 @@ use crate::{ }; use collections::HashMap; use db::kvp::KEY_VALUE_STORE; +use editor::commit_tooltip::CommitTooltip; use editor::{ actions::MoveToEnd, scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar, }; +use git::repository::{CommitDetails, ResetMode}; use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged}; use gpui::*; -use language::{Buffer, File}; +use language::{markdown, Buffer, File, ParsedMarkdown}; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use multi_buffer::ExcerptInfo; use panel::{panel_editor_container, panel_editor_style, panel_filled_button, PanelHeader}; @@ -23,6 +25,7 @@ use project::{ use serde::{Deserialize, Serialize}; use settings::Settings as _; use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize}; +use time::OffsetDateTime; use ui::{ prelude::*, ButtonLike, Checkbox, CheckboxWithLabel, Divider, DividerColor, ElevationIndex, IndentGuideColors, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip, @@ -207,7 +210,7 @@ impl GitPanel { ) -> Entity { let fs = workspace.app_state().fs.clone(); let project = workspace.project().clone(); - let git_state = project.read(cx).git_state().clone(); + let git_store = project.read(cx).git_store().clone(); let active_repository = project.read(cx).active_repository(cx); let workspace = cx.entity().downgrade(); @@ -231,14 +234,14 @@ impl GitPanel { let scroll_handle = UniformListScrollHandle::new(); cx.subscribe_in( - &git_state, + &git_store, window, - move |this, git_state, event, window, cx| match event { + move |this, git_store, event, window, cx| match event { GitEvent::FileSystemUpdated => { this.schedule_update(false, window, cx); } GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => { - this.active_repository = git_state.read(cx).active_repository(); + this.active_repository = git_store.read(cx).active_repository(); this.schedule_update(true, window, cx); } }, @@ -744,6 +747,40 @@ impl GitPanel { self.pending_commit = Some(task); } + fn uncommit(&mut self, window: &mut Window, cx: &mut Context) { + let Some(repo) = self.active_repository.clone() else { + return; + }; + let prior_head = self.load_commit_details("HEAD", cx); + + let task = cx.spawn(|_, mut cx| async move { + let prior_head = prior_head.await?; + + repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))? + .await??; + + Ok(prior_head) + }); + + let task = cx.spawn_in(window, |this, mut cx| async move { + let result = task.await; + this.update_in(&mut cx, |this, window, cx| { + this.pending_commit.take(); + match result { + Ok(prior_commit) => { + this.commit_editor.update(cx, |editor, cx| { + editor.set_text(prior_commit.message, window, cx) + }); + } + Err(e) => this.show_err_toast(e, cx), + } + }) + .ok(); + }); + + self.pending_commit = Some(task); + } + fn fill_co_authors(&mut self, _: &FillCoAuthors, window: &mut Window, cx: &mut Context) { const CO_AUTHOR_PREFIX: &str = "Co-authored-by: "; @@ -1131,16 +1168,10 @@ impl GitPanel { let all_repositories = self .project .read(cx) - .git_state() + .git_store() .read(cx) .all_repositories(); - let branch = self - .active_repository - .as_ref() - .and_then(|repository| repository.read(cx).branch()) - .unwrap_or_else(|| "(no current branch)".into()); - let has_repo_above = all_repositories.iter().any(|repo| { repo.read(cx) .repository_entry @@ -1148,26 +1179,7 @@ impl GitPanel { .is_above_project() }); - let icon_button = Button::new("branch-selector", branch) - .color(Color::Muted) - .style(ButtonStyle::Subtle) - .icon(IconName::GitBranch) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .size(ButtonSize::Compact) - .icon_position(IconPosition::Start) - .tooltip(Tooltip::for_action_title( - "Switch Branch", - &zed_actions::git::Branch, - )) - .on_click(cx.listener(|_, _, window, cx| { - window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); - })) - .style(ButtonStyle::Transparent); - self.panel_header_container(window, cx) - .child(h_flex().pl_1().child(icon_button)) - .child(div().flex_grow()) .when(all_repositories.len() > 1 || has_repo_above, |el| { el.child(self.render_repository_selector(cx)) }) @@ -1200,6 +1212,7 @@ impl GitPanel { && !editor.read(cx).is_empty(cx) && !self.has_unstaged_conflicts() && self.has_write_access(cx); + // let can_commit_all = // !self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx); let panel_editor_style = panel_editor_style(true, window, cx); @@ -1274,10 +1287,108 @@ impl GitPanel { ) } + fn render_previous_commit(&self, cx: &mut Context) -> Option { + let active_repository = self.active_repository.as_ref()?; + let branch = active_repository.read(cx).branch()?; + let commit = branch.most_recent_commit.as_ref()?.clone(); + + if branch.upstream.as_ref().is_some_and(|upstream| { + if let Some(tracking) = &upstream.tracking { + tracking.ahead == 0 + } else { + true + } + }) { + return None; + } + + let _branch_selector = Button::new("branch-selector", branch.name.clone()) + .color(Color::Muted) + .style(ButtonStyle::Subtle) + .icon(IconName::GitBranch) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .size(ButtonSize::Compact) + .icon_position(IconPosition::Start) + .tooltip(Tooltip::for_action_title( + "Switch Branch", + &zed_actions::git::Branch, + )) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); + })) + .style(ButtonStyle::Transparent); + + let _timestamp = Label::new(time_format::format_local_timestamp( + OffsetDateTime::from_unix_timestamp(commit.commit_timestamp).log_err()?, + OffsetDateTime::now_utc(), + time_format::TimestampFormat::Relative, + )) + .size(LabelSize::Small) + .color(Color::Muted); + + let tooltip = if self.has_staged_changes() { + "git reset HEAD^ --soft" + } else { + "git reset HEAD^" + }; + + let this = cx.entity(); + Some( + h_flex() + .items_center() + .py_1p5() + .px(px(8.)) + .bg(cx.theme().colors().background) + .border_t_1() + .border_color(cx.theme().colors().border) + .gap_1p5() + .child( + div() + .flex_grow() + .overflow_hidden() + .max_w(relative(0.6)) + .h_full() + .child( + Label::new(commit.subject.clone()) + .size(LabelSize::Small) + .text_ellipsis(), + ) + .id("commit-msg-hover") + .hoverable_tooltip(move |window, cx| { + GitPanelMessageTooltip::new( + this.clone(), + commit.sha.clone(), + window, + cx, + ) + .into() + }), + ) + .child(div().flex_1()) + .child( + panel_filled_button("Uncommit") + .icon(IconName::Undo) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .tooltip(Tooltip::for_action_title(tooltip, &git::Uncommit)) + .on_click(cx.listener(|this, _, window, cx| this.uncommit(window, cx))), + // .child( + // panel_filled_button("Push") + // .icon(IconName::ArrowUp) + // .icon_size(IconSize::Small) + // .icon_color(Color::Muted) + // .icon_position(IconPosition::Start), // .disabled(true), + // ), + ), + ) + } + fn render_empty_state(&self, cx: &mut Context) -> impl IntoElement { h_flex() .h_full() - .flex_1() + .flex_grow() .justify_center() .items_center() .child( @@ -1563,6 +1674,17 @@ impl GitPanel { .into_any_element() } + fn load_commit_details( + &self, + sha: &str, + cx: &mut Context, + ) -> Task> { + let Some(repo) = self.active_repository.clone() else { + return Task::ready(Err(anyhow::anyhow!("no active repo"))); + }; + repo.update(cx, |repo, cx| repo.show(sha, cx)) + } + fn render_entry( &self, ix: usize, @@ -1757,6 +1879,7 @@ impl Render for GitPanel { } else { self.render_empty_state(cx).into_any_element() }) + .children(self.render_previous_commit(cx)) .child(self.render_commit_editor(window, cx)) } } @@ -1843,3 +1966,81 @@ impl Panel for GitPanel { } impl PanelHeader for GitPanel {} + +struct GitPanelMessageTooltip { + commit_tooltip: Option>, +} + +impl GitPanelMessageTooltip { + fn new( + git_panel: Entity, + sha: SharedString, + window: &mut Window, + cx: &mut App, + ) -> Entity { + let workspace = git_panel.read(cx).workspace.clone(); + cx.new(|cx| { + cx.spawn_in(window, |this, mut cx| async move { + let language_registry = workspace.update(&mut cx, |workspace, _cx| { + workspace.app_state().languages.clone() + })?; + + let details = git_panel + .update(&mut cx, |git_panel, cx| { + git_panel.load_commit_details(&sha, cx) + })? + .await?; + + let mut parsed_message = ParsedMarkdown::default(); + markdown::parse_markdown_block( + &details.message, + Some(&language_registry), + None, + &mut parsed_message.text, + &mut parsed_message.highlights, + &mut parsed_message.region_ranges, + &mut parsed_message.regions, + ) + .await; + + let commit_details = editor::commit_tooltip::CommitDetails { + sha: details.sha.clone(), + committer_name: details.committer_name.clone(), + committer_email: details.committer_email.clone(), + commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?, + message: Some(editor::commit_tooltip::ParsedCommitMessage { + message: details.message.clone(), + parsed_message, + ..Default::default() + }), + }; + + this.update_in(&mut cx, |this: &mut GitPanelMessageTooltip, window, cx| { + this.commit_tooltip = Some(cx.new(move |cx| { + CommitTooltip::new( + commit_details, + panel_editor_style(true, window, cx), + Some(workspace), + ) + })); + cx.notify(); + }) + }) + .detach(); + + Self { + commit_tooltip: None, + } + }) + } +} + +impl Render for GitPanelMessageTooltip { + fn render(&mut self, _window: &mut Window, _cx: &mut Context<'_, Self>) -> impl IntoElement { + if let Some(commit_tooltip) = &self.commit_tooltip { + commit_tooltip.clone().into_any_element() + } else { + gpui::Empty.into_any_element() + } + } +} diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 2f812de686e1d6..8b8907ecbe5e68 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -12,7 +12,7 @@ use gpui::{ }; use language::{Anchor, Buffer, Capability, OffsetRangeExt, Point}; use multi_buffer::{MultiBuffer, PathKey}; -use project::{git::GitState, Project, ProjectPath}; +use project::{git::GitStore, Project, ProjectPath}; use theme::ActiveTheme; use ui::prelude::*; use util::ResultExt as _; @@ -31,7 +31,7 @@ pub(crate) struct ProjectDiff { editor: Entity, project: Entity, git_panel: Entity, - git_state: Entity, + git_store: Entity, workspace: WeakEntity, focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, @@ -137,11 +137,11 @@ impl ProjectDiff { cx.subscribe_in(&editor, window, Self::handle_editor_event) .detach(); - let git_state = project.read(cx).git_state().clone(); - let git_state_subscription = cx.subscribe_in( - &git_state, + let git_store = project.read(cx).git_store().clone(); + let git_store_subscription = cx.subscribe_in( + &git_store, window, - move |this, _git_state, _event, _window, _cx| { + move |this, _git_store, _event, _window, _cx| { *this.update_needed.borrow_mut() = (); }, ); @@ -156,7 +156,7 @@ impl ProjectDiff { Self { project, - git_state: git_state.clone(), + git_store: git_store.clone(), git_panel: git_panel.clone(), workspace: workspace.downgrade(), focus_handle, @@ -165,7 +165,7 @@ impl ProjectDiff { pending_scroll: None, update_needed: send, _task: worker, - _subscription: git_state_subscription, + _subscription: git_store_subscription, } } @@ -175,7 +175,7 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context, ) { - let Some(git_repo) = self.git_state.read(cx).active_repository() else { + let Some(git_repo) = self.git_store.read(cx).active_repository() else { return; }; let repo = git_repo.read(cx); @@ -248,7 +248,7 @@ impl ProjectDiff { } fn load_buffers(&mut self, cx: &mut Context) -> Vec>> { - let Some(repo) = self.git_state.read(cx).active_repository() else { + let Some(repo) = self.git_store.read(cx).active_repository() else { self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.clear(cx); }); diff --git a/crates/git_ui/src/quick_commit.rs b/crates/git_ui/src/quick_commit.rs index be7f3fa84db404..cd8a3154963f66 100644 --- a/crates/git_ui/src/quick_commit.rs +++ b/crates/git_ui/src/quick_commit.rs @@ -98,7 +98,7 @@ impl QuickCommitModal { commit_message_buffer: Option>, cx: &mut Context, ) -> Self { - let git_state = project.read(cx).git_state().clone(); + let git_store = project.read(cx).git_store().clone(); let active_repository = project.read(cx).active_repository(cx); let focus_handle = cx.focus_handle(); @@ -130,7 +130,7 @@ impl QuickCommitModal { let all_repositories = self .project .read(cx) - .git_state() + .git_store() .read(cx) .all_repositories(); let entry_count = self diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index e5d9c1839a90bf..8c27c605194ba9 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -4,7 +4,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; use project::{ - git::{GitState, Repository}, + git::{GitStore, Repository}, Project, }; use std::sync::Arc; @@ -20,8 +20,8 @@ pub struct RepositorySelector { impl RepositorySelector { pub fn new(project: Entity, window: &mut Window, cx: &mut Context) -> Self { - let git_state = project.read(cx).git_state().clone(); - let all_repositories = git_state.read(cx).all_repositories(); + let git_store = project.read(cx).git_store().clone(); + let all_repositories = git_store.read(cx).all_repositories(); let filtered_repositories = all_repositories.clone(); let delegate = RepositorySelectorDelegate { project: project.downgrade(), @@ -38,7 +38,7 @@ impl RepositorySelector { }); let _subscriptions = - vec![cx.subscribe_in(&git_state, window, Self::handle_project_git_event)]; + vec![cx.subscribe_in(&git_store, window, Self::handle_project_git_event)]; RepositorySelector { picker, @@ -49,7 +49,7 @@ impl RepositorySelector { fn handle_project_git_event( &mut self, - git_state: &Entity, + git_store: &Entity, _event: &project::git::GitEvent, window: &mut Window, cx: &mut Context, @@ -57,7 +57,7 @@ impl RepositorySelector { // TODO handle events individually let task = self.picker.update(cx, |this, cx| { let query = this.query(cx); - this.delegate.repository_entries = git_state.read(cx).all_repositories(); + this.delegate.repository_entries = git_store.read(cx).all_repositories(); this.delegate.update_matches(query, window, cx) }); self.update_matches_task = Some(task); diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 138d83f078b00b..7654b6902978cc 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -2722,8 +2722,8 @@ fn serialize_blame_buffer_response(blame: Option) -> proto::B author_mail: entry.author_mail.clone(), author_time: entry.author_time, author_tz: entry.author_tz.clone(), - committer: entry.committer.clone(), - committer_mail: entry.committer_mail.clone(), + committer: entry.committer_name.clone(), + committer_mail: entry.committer_email.clone(), committer_time: entry.committer_time, committer_tz: entry.committer_tz.clone(), summary: entry.summary.clone(), @@ -2772,10 +2772,10 @@ fn deserialize_blame_buffer_response( sha: git::Oid::from_bytes(&entry.sha).ok()?, range: entry.start_line..entry.end_line, original_line_number: entry.original_line_number, - committer: entry.committer, + committer_name: entry.committer, committer_time: entry.committer_time, committer_tz: entry.committer_tz, - committer_mail: entry.committer_mail, + committer_email: entry.committer_mail, author: entry.author, author_mail: entry.author_mail, author_time: entry.author_time, diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index f420a2b9290e39..61b58fc133298b 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -1,20 +1,22 @@ use crate::buffer_store::BufferStore; use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent}; use crate::{Project, ProjectPath}; -use anyhow::Context as _; +use anyhow::{Context as _, Result}; use client::ProjectId; use futures::channel::{mpsc, oneshot}; use futures::StreamExt as _; +use git::repository::{Branch, CommitDetails, ResetMode}; use git::{ repository::{GitRepository, RepoPath}, status::{GitSummary, TrackedSummary}, }; use gpui::{ - App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity, + App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, + WeakEntity, }; use language::{Buffer, LanguageRegistry}; -use rpc::proto::ToProto; -use rpc::{proto, AnyProtoClient}; +use rpc::proto::{git_reset, ToProto}; +use rpc::{proto, AnyProtoClient, TypedEnvelope}; use settings::WorktreeId; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -22,22 +24,23 @@ use text::BufferId; use util::{maybe, ResultExt}; use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry}; -pub struct GitState { +pub struct GitStore { pub(super) project_id: Option, pub(super) client: Option, - pub update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender>)>, + buffer_store: Entity, repositories: Vec>, active_index: Option, + update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender>)>, _subscription: Subscription, } pub struct Repository { commit_message_buffer: Option>, - git_state: WeakEntity, + git_store: WeakEntity, pub worktree_id: WorktreeId, pub repository_entry: RepositoryEntry, pub git_repo: GitRepo, - update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender>)>, + update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender>)>, } #[derive(Clone)] @@ -57,6 +60,11 @@ pub enum Message { message: SharedString, name_and_email: Option<(SharedString, SharedString)>, }, + Reset { + repo: GitRepo, + commit: SharedString, + reset_mode: ResetMode, + }, Stage(GitRepo, Vec), Unstage(GitRepo, Vec), SetIndexText(GitRepo, RepoPath, Option), @@ -68,11 +76,12 @@ pub enum GitEvent { GitStateUpdated, } -impl EventEmitter for GitState {} +impl EventEmitter for GitStore {} -impl GitState { +impl GitStore { pub fn new( worktree_store: &Entity, + buffer_store: Entity, client: Option, project_id: Option, cx: &mut Context<'_, Self>, @@ -80,9 +89,10 @@ impl GitState { let update_sender = Self::spawn_git_worker(cx); let _subscription = cx.subscribe(worktree_store, Self::on_worktree_store_event); - GitState { + GitStore { project_id, client, + buffer_store, repositories: Vec::new(), active_index: None, update_sender, @@ -90,6 +100,16 @@ impl GitState { } } + pub fn init(client: &AnyProtoClient) { + client.add_entity_request_handler(Self::handle_stage); + client.add_entity_request_handler(Self::handle_unstage); + client.add_entity_request_handler(Self::handle_commit); + client.add_entity_request_handler(Self::handle_reset); + client.add_entity_request_handler(Self::handle_show); + client.add_entity_request_handler(Self::handle_open_commit_message_buffer); + client.add_entity_request_handler(Self::handle_set_index_text); + } + pub fn active_repository(&self) -> Option> { self.active_index .map(|index| self.repositories[index].clone()) @@ -153,7 +173,7 @@ impl GitState { existing_handle } else { cx.new(|_| Repository { - git_state: this.clone(), + git_store: this.clone(), worktree_id, repository_entry: repo.clone(), git_repo, @@ -189,10 +209,10 @@ impl GitState { } fn spawn_git_worker( - cx: &mut Context<'_, GitState>, - ) -> mpsc::UnboundedSender<(Message, oneshot::Sender>)> { + cx: &mut Context<'_, GitStore>, + ) -> mpsc::UnboundedSender<(Message, oneshot::Sender>)> { let (update_sender, mut update_receiver) = - mpsc::unbounded::<(Message, oneshot::Sender>)>(); + mpsc::unbounded::<(Message, oneshot::Sender>)>(); cx.spawn(|_, cx| async move { while let Some((msg, respond)) = update_receiver.next().await { let result = cx @@ -206,7 +226,7 @@ impl GitState { update_sender } - async fn process_git_msg(msg: Message) -> Result<(), anyhow::Error> { + async fn process_git_msg(msg: Message) -> Result<()> { match msg { Message::Stage(repo, paths) => { match repo { @@ -233,6 +253,35 @@ impl GitState { } Ok(()) } + Message::Reset { + repo, + commit, + reset_mode, + } => { + match repo { + GitRepo::Local(repo) => repo.reset(&commit, reset_mode)?, + GitRepo::Remote { + project_id, + client, + worktree_id, + work_directory_id, + } => { + client + .request(proto::GitReset { + project_id: project_id.0, + worktree_id: worktree_id.to_proto(), + work_directory_id: work_directory_id.to_proto(), + commit: commit.into(), + mode: match reset_mode { + ResetMode::Soft => git_reset::ResetMode::Soft.into(), + ResetMode::Mixed => git_reset::ResetMode::Mixed.into(), + }, + }) + .await?; + } + } + Ok(()) + } Message::Unstage(repo, paths) => { match repo { GitRepo::Local(repo) => repo.unstage_paths(&paths)?, @@ -309,20 +358,219 @@ impl GitState { }, } } + + async fn handle_stage( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); + let repository_handle = + Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + + let entries = envelope + .payload + .paths + .into_iter() + .map(PathBuf::from) + .map(RepoPath::new) + .collect(); + + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.stage_entries(entries) + })? + .await??; + Ok(proto::Ack {}) + } + + async fn handle_unstage( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); + let repository_handle = + Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + + let entries = envelope + .payload + .paths + .into_iter() + .map(PathBuf::from) + .map(RepoPath::new) + .collect(); + + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.unstage_entries(entries) + })? + .await??; + + Ok(proto::Ack {}) + } + + async fn handle_set_index_text( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); + let repository_handle = + Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.set_index_text( + &RepoPath::from_str(&envelope.payload.path), + envelope.payload.text, + ) + })? + .await??; + Ok(proto::Ack {}) + } + + async fn handle_commit( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); + let repository_handle = + Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + + let message = SharedString::from(envelope.payload.message); + let name = envelope.payload.name.map(SharedString::from); + let email = envelope.payload.email.map(SharedString::from); + + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.commit(message, name.zip(email)) + })? + .await??; + Ok(proto::Ack {}) + } + + async fn handle_show( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); + let repository_handle = + Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + + let commit = repository_handle + .update(&mut cx, |repository_handle, cx| { + repository_handle.show(&envelope.payload.commit, cx) + })? + .await?; + Ok(proto::GitCommitDetails { + sha: commit.sha.into(), + message: commit.message.into(), + commit_timestamp: commit.commit_timestamp, + committer_email: commit.committer_email.into(), + committer_name: commit.committer_name.into(), + }) + } + + async fn handle_reset( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); + let repository_handle = + Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + + let mode = match envelope.payload.mode() { + git_reset::ResetMode::Soft => ResetMode::Soft, + git_reset::ResetMode::Mixed => ResetMode::Mixed, + }; + + repository_handle + .update(&mut cx, |repository_handle, _| { + repository_handle.reset(&envelope.payload.commit, mode) + })? + .await??; + Ok(proto::Ack {}) + } + + async fn handle_open_commit_message_buffer( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); + let repository = + Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; + let buffer = repository + .update(&mut cx, |repository, cx| { + repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx) + })? + .await?; + + let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; + this.update(&mut cx, |this, cx| { + this.buffer_store.update(cx, |buffer_store, cx| { + buffer_store + .create_buffer_for_peer( + &buffer, + envelope.original_sender_id.unwrap_or(envelope.sender_id), + cx, + ) + .detach_and_log_err(cx); + }) + })?; + + Ok(proto::OpenBufferResponse { + buffer_id: buffer_id.to_proto(), + }) + } + + fn repository_for_request( + this: &Entity, + worktree_id: WorktreeId, + work_directory_id: ProjectEntryId, + cx: &mut AsyncApp, + ) -> Result> { + this.update(cx, |this, cx| { + let repository_handle = this + .all_repositories() + .into_iter() + .find(|repository_handle| { + repository_handle.read(cx).worktree_id == worktree_id + && repository_handle + .read(cx) + .repository_entry + .work_directory_id() + == work_directory_id + }) + .context("missing repository handle")?; + anyhow::Ok(repository_handle) + })? + } } impl GitRepo {} impl Repository { - pub fn git_state(&self) -> Option> { - self.git_state.upgrade() + pub fn git_store(&self) -> Option> { + self.git_store.upgrade() } fn id(&self) -> (WorktreeId, ProjectEntryId) { (self.worktree_id, self.repository_entry.work_directory_id()) } - pub fn branch(&self) -> Option> { + pub fn branch(&self) -> Option<&Branch> { self.repository_entry.branch() } @@ -344,19 +592,19 @@ impl Repository { } pub fn activate(&self, cx: &mut Context) { - let Some(git_state) = self.git_state.upgrade() else { + let Some(git_store) = self.git_store.upgrade() else { return; }; let entity = cx.entity(); - git_state.update(cx, |git_state, cx| { - let Some(index) = git_state + git_store.update(cx, |git_store, cx| { + let Some(index) = git_store .repositories .iter() .position(|handle| *handle == entity) else { return; }; - git_state.active_index = Some(index); + git_store.active_index = Some(index); cx.emit(GitEvent::ActiveRepositoryChanged); }); } @@ -396,7 +644,7 @@ impl Repository { languages: Option>, buffer_store: Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { if let Some(buffer) = self.commit_message_buffer.clone() { return Task::ready(Ok(buffer)); } @@ -444,7 +692,7 @@ impl Repository { language_registry: Option>, buffer_store: Entity, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { cx.spawn(|repository, mut cx| async move { let buffer = buffer_store .update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))? @@ -464,7 +712,57 @@ impl Repository { }) } - pub fn stage_entries(&self, entries: Vec) -> oneshot::Receiver> { + pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver> { + let (result_tx, result_rx) = futures::channel::oneshot::channel(); + let commit = commit.to_string().into(); + self.update_sender + .unbounded_send(( + Message::Reset { + repo: self.git_repo.clone(), + commit, + reset_mode, + }, + result_tx, + )) + .ok(); + result_rx + } + + pub fn show(&self, commit: &str, cx: &Context) -> Task> { + let commit = commit.to_string(); + match self.git_repo.clone() { + GitRepo::Local(git_repository) => { + let commit = commit.to_string(); + cx.background_executor() + .spawn(async move { git_repository.show(&commit) }) + } + GitRepo::Remote { + project_id, + client, + worktree_id, + work_directory_id, + } => cx.background_executor().spawn(async move { + let resp = client + .request(proto::GitShow { + project_id: project_id.0, + worktree_id: worktree_id.to_proto(), + work_directory_id: work_directory_id.to_proto(), + commit, + }) + .await?; + + Ok(CommitDetails { + sha: resp.sha.into(), + message: resp.message.into(), + commit_timestamp: resp.commit_timestamp, + committer_email: resp.committer_email.into(), + committer_name: resp.committer_name.into(), + }) + }), + } + } + + pub fn stage_entries(&self, entries: Vec) -> oneshot::Receiver> { let (result_tx, result_rx) = futures::channel::oneshot::channel(); if entries.is_empty() { result_tx.send(Ok(())).ok(); @@ -476,7 +774,7 @@ impl Repository { result_rx } - pub fn unstage_entries(&self, entries: Vec) -> oneshot::Receiver> { + pub fn unstage_entries(&self, entries: Vec) -> oneshot::Receiver> { let (result_tx, result_rx) = futures::channel::oneshot::channel(); if entries.is_empty() { result_tx.send(Ok(())).ok(); @@ -488,7 +786,7 @@ impl Repository { result_rx } - pub fn stage_all(&self) -> oneshot::Receiver> { + pub fn stage_all(&self) -> oneshot::Receiver> { let to_stage = self .repository_entry .status() @@ -498,7 +796,7 @@ impl Repository { self.stage_entries(to_stage) } - pub fn unstage_all(&self) -> oneshot::Receiver> { + pub fn unstage_all(&self) -> oneshot::Receiver> { let to_unstage = self .repository_entry .status() @@ -530,7 +828,7 @@ impl Repository { &self, message: SharedString, name_and_email: Option<(SharedString, SharedString)>, - ) -> oneshot::Receiver> { + ) -> oneshot::Receiver> { let (result_tx, result_rx) = futures::channel::oneshot::channel(); self.update_sender .unbounded_send(( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d09ef9bd8f0af1..8cc0481a5a6981 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -27,7 +27,7 @@ use git::Repository; pub mod search_history; mod yarn; -use crate::git::GitState; +use crate::git::GitStore; use anyhow::{anyhow, Context as _, Result}; use buffer_store::{BufferStore, BufferStoreEvent}; use client::{ @@ -161,7 +161,7 @@ pub struct Project { fs: Arc, ssh_client: Option>, client_state: ProjectClientState, - git_state: Entity, + git_store: Entity, collaborators: HashMap, client_subscriptions: Vec, worktree_store: Entity, @@ -610,15 +610,10 @@ impl Project { client.add_entity_request_handler(Self::handle_open_new_buffer); client.add_entity_message_handler(Self::handle_create_buffer_for_peer); - client.add_entity_request_handler(Self::handle_stage); - client.add_entity_request_handler(Self::handle_unstage); - client.add_entity_request_handler(Self::handle_commit); - client.add_entity_request_handler(Self::handle_set_index_text); - client.add_entity_request_handler(Self::handle_open_commit_message_buffer); - WorktreeStore::init(&client); BufferStore::init(&client); LspStore::init(&client); + GitStore::init(&client); SettingsObserver::init(&client); TaskStore::init(Some(&client)); ToolchainStore::init(&client); @@ -705,7 +700,8 @@ impl Project { ) }); - let git_state = cx.new(|cx| GitState::new(&worktree_store, None, None, cx)); + let git_store = + cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx)); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); @@ -718,7 +714,7 @@ impl Project { lsp_store, join_project_response_message_id: 0, client_state: ProjectClientState::Local, - git_state, + git_store, client_subscriptions: Vec::new(), _subscriptions: vec![cx.on_release(Self::release)], active_entry: None, @@ -825,9 +821,10 @@ impl Project { }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - let git_state = cx.new(|cx| { - GitState::new( + let git_store = cx.new(|cx| { + GitStore::new( &worktree_store, + buffer_store.clone(), Some(ssh_proto.clone()), Some(ProjectId(SSH_PROJECT_ID)), cx, @@ -846,7 +843,7 @@ impl Project { lsp_store, join_project_response_message_id: 0, client_state: ProjectClientState::Local, - git_state, + git_store, client_subscriptions: Vec::new(), _subscriptions: vec![ cx.on_release(Self::release), @@ -896,6 +893,7 @@ impl Project { ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); + ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.git_store); ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer); ssh_proto.add_entity_message_handler(Self::handle_update_worktree); @@ -909,6 +907,7 @@ impl Project { SettingsObserver::init(&ssh_proto); TaskStore::init(Some(&ssh_proto)); ToolchainStore::init(&ssh_proto); + GitStore::init(&ssh_proto); this }) @@ -1030,9 +1029,10 @@ impl Project { SettingsObserver::new_remote(worktree_store.clone(), task_store.clone(), cx) })?; - let git_state = cx.new(|cx| { - GitState::new( + let git_store = cx.new(|cx| { + GitStore::new( &worktree_store, + buffer_store.clone(), Some(client.clone().into()), Some(ProjectId(remote_id)), cx, @@ -1089,7 +1089,7 @@ impl Project { remote_id, replica_id, }, - git_state, + git_store, buffers_needing_diff: Default::default(), git_diff_debouncer: DebouncedDelay::new(), terminals: Terminals { @@ -1675,6 +1675,9 @@ impl Project { self.client .subscribe_to_entity(project_id)? .set_entity(&self.settings_observer, &mut cx.to_async()), + self.client + .subscribe_to_entity(project_id)? + .set_entity(&self.git_store, &mut cx.to_async()), ]); self.buffer_store.update(cx, |buffer_store, cx| { @@ -4038,142 +4041,6 @@ impl Project { Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx) } - async fn handle_stage( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - - let entries = envelope - .payload - .paths - .into_iter() - .map(PathBuf::from) - .map(RepoPath::new) - .collect(); - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.stage_entries(entries) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_unstage( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - - let entries = envelope - .payload - .paths - .into_iter() - .map(PathBuf::from) - .map(RepoPath::new) - .collect(); - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.unstage_entries(entries) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_commit( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - - let message = SharedString::from(envelope.payload.message); - let name = envelope.payload.name.map(SharedString::from); - let email = envelope.payload.email.map(SharedString::from); - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.commit(message, name.zip(email)) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_set_index_text( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.set_index_text( - &RepoPath::from_str(&envelope.payload.path), - envelope.payload.text, - ) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_open_commit_message_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - let buffer = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx) - })? - .await?; - - let peer_id = envelope.original_sender_id()?; - Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx) - } - - fn repository_for_request( - this: &Entity, - worktree_id: WorktreeId, - work_directory_id: ProjectEntryId, - cx: &mut AsyncApp, - ) -> Result> { - this.update(cx, |project, cx| { - let repository_handle = project - .git_state() - .read(cx) - .all_repositories() - .into_iter() - .find(|repository_handle| { - let repository_handle = repository_handle.read(cx); - repository_handle.worktree_id == worktree_id - && repository_handle.repository_entry.work_directory_id() - == work_directory_id - }) - .context("missing repository handle")?; - anyhow::Ok(repository_handle) - })? - } - fn respond_to_open_buffer_request( this: Entity, buffer: Entity, @@ -4365,16 +4232,16 @@ impl Project { &self.buffer_store } - pub fn git_state(&self) -> &Entity { - &self.git_state + pub fn git_store(&self) -> &Entity { + &self.git_store } pub fn active_repository(&self, cx: &App) -> Option> { - self.git_state.read(cx).active_repository() + self.git_store.read(cx).active_repository() } pub fn all_repositories(&self, cx: &App) -> Vec> { - self.git_state.read(cx).all_repositories() + self.git_store.read(cx).all_repositories() } pub fn repository_and_path_for_buffer_id( @@ -4386,7 +4253,7 @@ impl Project { .buffer_for_id(buffer_id, cx)? .read(cx) .project_path(cx)?; - self.git_state + self.git_store .read(cx) .all_repositories() .into_iter() diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 6461d97723b9f3..73b49775e64bd4 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -12,6 +12,7 @@ use futures::{ future::{BoxFuture, Shared}, FutureExt, SinkExt, }; +use git::repository::Branch; use gpui::{App, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, WeakEntity}; use postage::oneshot; use rpc::{ @@ -24,7 +25,10 @@ use smol::{ }; use text::ReplicaId; use util::{paths::SanitizedPath, ResultExt}; -use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings}; +use worktree::{ + branch_to_proto, Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, + WorktreeSettings, +}; use crate::{search::SearchQuery, ProjectPath}; @@ -133,11 +137,12 @@ impl WorktreeStore { .find(|worktree| worktree.read(cx).id() == id) } - pub fn current_branch(&self, repository: ProjectPath, cx: &App) -> Option> { + pub fn current_branch(&self, repository: ProjectPath, cx: &App) -> Option { self.worktree_for_id(repository.worktree_id, cx)? .read(cx) .git_entry(repository.path)? .branch() + .cloned() } pub fn worktree_for_entry( @@ -938,9 +943,24 @@ impl WorktreeStore { .map(|proto_branch| git::repository::Branch { is_head: proto_branch.is_head, name: proto_branch.name.into(), - unix_timestamp: proto_branch - .unix_timestamp - .map(|timestamp| timestamp as i64), + upstream: proto_branch.upstream.map(|upstream| { + git::repository::Upstream { + ref_name: upstream.ref_name.into(), + tracking: upstream.tracking.map(|tracking| { + git::repository::UpstreamTracking { + ahead: tracking.ahead as u32, + behind: tracking.behind as u32, + } + }), + } + }), + most_recent_commit: proto_branch.most_recent_commit.map(|commit| { + git::repository::CommitSummary { + sha: commit.sha.into(), + subject: commit.subject.into(), + commit_timestamp: commit.commit_timestamp, + } + }), }) .collect(); @@ -1126,14 +1146,7 @@ impl WorktreeStore { .await?; Ok(proto::GitBranchesResponse { - branches: branches - .into_iter() - .map(|branch| proto::Branch { - is_head: branch.is_head, - name: branch.name.to_string(), - unix_timestamp: branch.unix_timestamp.map(|timestamp| timestamp as u64), - }) - .collect(), + branches: branches.iter().map(branch_to_proto).collect(), }) } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 4fdfa0ae928a64..c86459f2cd9417 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -316,6 +316,9 @@ message Envelope { OpenUncommittedDiff open_uncommitted_diff = 297; OpenUncommittedDiffResponse open_uncommitted_diff_response = 298; + GitShow git_show = 300; + GitReset git_reset = 301; + GitCommitDetails git_commit_details = 302; SetIndexText set_index_text = 299; // current max } @@ -1800,12 +1803,14 @@ message Entry { message RepositoryEntry { uint64 work_directory_id = 1; - optional string branch = 2; + optional string branch = 2; // deprecated + optional Branch branch_summary = 6; repeated StatusEntry updated_statuses = 3; repeated string removed_statuses = 4; repeated string current_merge_conflicts = 5; } + message StatusEntry { string repo_path = 1; // Can be removed once collab's min version is >=0.171.0. @@ -2615,10 +2620,26 @@ message ActiveToolchainResponse { optional Toolchain toolchain = 1; } +message CommitSummary { + string sha = 1; + string subject = 2; + int64 commit_timestamp = 3; +} + message Branch { bool is_head = 1; string name = 2; optional uint64 unix_timestamp = 3; + optional GitUpstream upstream = 4; + optional CommitSummary most_recent_commit = 5; +} +message GitUpstream { + string ref_name = 1; + optional UpstreamTracking tracking = 2; +} +message UpstreamTracking { + uint64 ahead = 1; + uint64 behind = 2; } message GitBranches { @@ -2639,6 +2660,33 @@ message UpdateGitBranch { message GetPanicFiles { } +message GitShow { + uint64 project_id = 1; + uint64 worktree_id = 2; + uint64 work_directory_id = 3; + string commit = 4; +} + +message GitCommitDetails { + string sha = 1; + string message = 2; + int64 commit_timestamp = 3; + string committer_email = 4; + string committer_name = 5; +} + +message GitReset { + uint64 project_id = 1; + uint64 worktree_id = 2; + uint64 work_directory_id = 3; + string commit = 4; + ResetMode mode = 5; + enum ResetMode { + SOFT = 0; + MIXED = 1; + } +} + message GetPanicFilesResponse { repeated string file_contents = 2; } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index b51f34914b6a08..0743e92da685a5 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -440,6 +440,9 @@ messages!( (SyncExtensionsResponse, Background), (InstallExtension, Background), (RegisterBufferWithLanguageServers, Background), + (GitReset, Background), + (GitShow, Background), + (GitCommitDetails, Background), (SetIndexText, Background), ); @@ -574,6 +577,8 @@ request_messages!( (SyncExtensions, SyncExtensionsResponse), (InstallExtension, Ack), (RegisterBufferWithLanguageServers, Ack), + (GitShow, GitCommitDetails), + (GitReset, Ack), (SetIndexText, Ack), ); @@ -667,6 +672,8 @@ entity_messages!( GetPathMetadata, CancelLanguageServerWork, RegisterBufferWithLanguageServers, + GitShow, + GitReset, SetIndexText, ); diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index e274014c1e4a7a..62523443f5aea8 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,22 +1,20 @@ use ::proto::{FromProto, ToProto}; -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{anyhow, Result}; use extension::ExtensionHostProxy; use extension_host::headless_host::HeadlessExtensionStore; use fs::Fs; -use git::repository::RepoPath; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel, SharedString}; +use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel}; use http_client::HttpClient; use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry}; use node_runtime::NodeRuntime; use project::{ buffer_store::{BufferStore, BufferStoreEvent}, - git::{GitState, Repository}, + git::GitStore, project_settings::SettingsObserver, search::SearchQuery, task_store::TaskStore, worktree_store::WorktreeStore, - LspStore, LspStoreEvent, PrettierStore, ProjectEntryId, ProjectPath, ToolchainStore, - WorktreeId, + LspStore, LspStoreEvent, PrettierStore, ProjectPath, ToolchainStore, WorktreeId, }; use remote::ssh_session::ChannelClient; use rpc::{ @@ -44,7 +42,7 @@ pub struct HeadlessProject { pub next_entry_id: Arc, pub languages: Arc, pub extensions: Entity, - pub git_state: Entity, + pub git_store: Entity, } pub struct HeadlessAppState { @@ -83,13 +81,14 @@ impl HeadlessProject { store }); - let git_state = cx.new(|cx| GitState::new(&worktree_store, None, None, cx)); - let buffer_store = cx.new(|cx| { let mut buffer_store = BufferStore::local(worktree_store.clone(), cx); buffer_store.shared(SSH_PROJECT_ID, session.clone().into(), cx); buffer_store }); + + let git_store = + cx.new(|cx| GitStore::new(&worktree_store, buffer_store.clone(), None, None, cx)); let prettier_store = cx.new(|cx| { PrettierStore::new( node_runtime.clone(), @@ -180,6 +179,7 @@ impl HeadlessProject { session.subscribe_to_entity(SSH_PROJECT_ID, &task_store); session.subscribe_to_entity(SSH_PROJECT_ID, &toolchain_store); session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); + session.subscribe_to_entity(SSH_PROJECT_ID, &git_store); client.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); client.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); @@ -197,12 +197,6 @@ impl HeadlessProject { client.add_entity_request_handler(BufferStore::handle_update_buffer); client.add_entity_message_handler(BufferStore::handle_close_buffer); - client.add_entity_request_handler(Self::handle_stage); - client.add_entity_request_handler(Self::handle_unstage); - client.add_entity_request_handler(Self::handle_commit); - client.add_entity_request_handler(Self::handle_set_index_text); - client.add_entity_request_handler(Self::handle_open_commit_message_buffer); - client.add_request_handler( extensions.clone().downgrade(), HeadlessExtensionStore::handle_sync_extensions, @@ -218,6 +212,7 @@ impl HeadlessProject { LspStore::init(&client); TaskStore::init(Some(&client)); ToolchainStore::init(&client); + GitStore::init(&client); HeadlessProject { session: client, @@ -230,7 +225,7 @@ impl HeadlessProject { next_entry_id: Default::default(), languages, extensions, - git_state, + git_store, } } @@ -616,157 +611,6 @@ impl HeadlessProject { log::debug!("Received ping from client"); Ok(proto::Ack {}) } - - async fn handle_stage( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - - let entries = envelope - .payload - .paths - .into_iter() - .map(PathBuf::from) - .map(RepoPath::new) - .collect(); - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.stage_entries(entries) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_unstage( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - - let entries = envelope - .payload - .paths - .into_iter() - .map(PathBuf::from) - .map(RepoPath::new) - .collect(); - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.unstage_entries(entries) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_commit( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository_handle = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - - let message = SharedString::from(envelope.payload.message); - let name = envelope.payload.name.map(SharedString::from); - let email = envelope.payload.email.map(SharedString::from); - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.commit(message, name.zip(email)) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_set_index_text( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - repository - .update(&mut cx, |repository, _| { - repository.set_index_text( - &RepoPath::from(envelope.payload.path.as_str()), - envelope.payload.text, - ) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_open_commit_message_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id); - let repository = - Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?; - let buffer = repository - .update(&mut cx, |repository, cx| { - repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx) - })? - .await?; - - let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; - this.update(&mut cx, |headless_project, cx| { - headless_project - .buffer_store - .update(cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer(&buffer, SSH_PEER_ID, cx) - .detach_and_log_err(cx); - }) - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - fn repository_for_request( - this: &Entity, - worktree_id: WorktreeId, - work_directory_id: ProjectEntryId, - cx: &mut AsyncApp, - ) -> Result> { - this.update(cx, |project, cx| { - let repository_handle = project - .git_state - .read(cx) - .all_repositories() - .into_iter() - .find(|repository_handle| { - repository_handle.read(cx).worktree_id == worktree_id - && repository_handle - .read(cx) - .repository_entry - .work_directory_id() - == work_directory_id - }) - .context("missing repository handle")?; - anyhow::Ok(repository_handle) - })? - } } fn prompt_to_proto( diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index c1a22b2c8a8b29..7cc6cea1dffdfc 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1364,7 +1364,7 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA }) }); - assert_eq!(server_branch.as_ref(), branches[2]); + assert_eq!(server_branch.name, branches[2]); // Also try creating a new branch cx.update(|cx| { @@ -1387,7 +1387,7 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA }) }); - assert_eq!(server_branch.as_ref(), "totally-new-branch"); + assert_eq!(server_branch.name, "totally-new-branch"); } pub async fn init_test( diff --git a/crates/time_format/src/time_format.rs b/crates/time_format/src/time_format.rs index bd5d96202649d4..fe9a96b87b3a07 100644 --- a/crates/time_format/src/time_format.rs +++ b/crates/time_format/src/time_format.rs @@ -24,19 +24,21 @@ pub fn format_localized_timestamp( ) -> String { let timestamp_local = timestamp.to_offset(timezone); let reference_local = reference.to_offset(timezone); + format_local_timestamp(timestamp_local, reference_local, format) +} +/// Formats a timestamp, which respects the user's date and time preferences/custom format. +pub fn format_local_timestamp( + timestamp: OffsetDateTime, + reference: OffsetDateTime, + format: TimestampFormat, +) -> String { match format { - TimestampFormat::Absolute => { - format_absolute_timestamp(timestamp_local, reference_local, false) - } - TimestampFormat::EnhancedAbsolute => { - format_absolute_timestamp(timestamp_local, reference_local, true) - } - TimestampFormat::MediumAbsolute => { - format_absolute_timestamp_medium(timestamp_local, reference_local) - } - TimestampFormat::Relative => format_relative_time(timestamp_local, reference_local) - .unwrap_or_else(|| format_relative_date(timestamp_local, reference_local)), + TimestampFormat::Absolute => format_absolute_timestamp(timestamp, reference, false), + TimestampFormat::EnhancedAbsolute => format_absolute_timestamp(timestamp, reference, true), + TimestampFormat::MediumAbsolute => format_absolute_timestamp_medium(timestamp, reference), + TimestampFormat::Relative => format_relative_time(timestamp, reference) + .unwrap_or_else(|| format_relative_date(timestamp, reference)), } } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 9f430585c41656..15375cb18e73d2 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -521,6 +521,7 @@ impl TitleBar { let branch_name = entry .as_ref() .and_then(|entry| entry.branch()) + .map(|branch| branch.name.clone()) .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?; Some( Button::new("project_branch_trigger", branch_name) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 88c61c9af6da1b..c3e444caf1d19b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -19,7 +19,7 @@ use futures::{ }; use fuzzy::CharBag; use git::{ - repository::{GitRepository, RepoPath}, + repository::{Branch, GitRepository, RepoPath}, status::{ FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode, }, @@ -201,7 +201,7 @@ pub struct RepositoryEntry { pub(crate) statuses_by_path: SumTree, work_directory_id: ProjectEntryId, pub work_directory: WorkDirectory, - pub(crate) branch: Option>, + pub(crate) branch: Option, pub current_merge_conflicts: TreeSet, } @@ -214,8 +214,8 @@ impl Deref for RepositoryEntry { } impl RepositoryEntry { - pub fn branch(&self) -> Option> { - self.branch.clone() + pub fn branch(&self) -> Option<&Branch> { + self.branch.as_ref() } pub fn work_directory_id(&self) -> ProjectEntryId { @@ -243,7 +243,8 @@ impl RepositoryEntry { pub fn initial_update(&self) -> proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: self.work_directory_id.to_proto(), - branch: self.branch.as_ref().map(|branch| branch.to_string()), + branch: self.branch.as_ref().map(|branch| branch.name.to_string()), + branch_summary: self.branch.as_ref().map(branch_to_proto), updated_statuses: self .statuses_by_path .iter() @@ -302,7 +303,8 @@ impl RepositoryEntry { proto::RepositoryEntry { work_directory_id: self.work_directory_id.to_proto(), - branch: self.branch.as_ref().map(|branch| branch.to_string()), + branch: self.branch.as_ref().map(|branch| branch.name.to_string()), + branch_summary: self.branch.as_ref().map(branch_to_proto), updated_statuses, removed_statuses, current_merge_conflicts: self @@ -314,6 +316,61 @@ impl RepositoryEntry { } } +pub fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch { + proto::Branch { + is_head: branch.is_head, + name: branch.name.to_string(), + unix_timestamp: branch + .most_recent_commit + .as_ref() + .map(|commit| commit.commit_timestamp as u64), + upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream { + ref_name: upstream.ref_name.to_string(), + tracking: upstream + .tracking + .as_ref() + .map(|upstream| proto::UpstreamTracking { + ahead: upstream.ahead as u64, + behind: upstream.behind as u64, + }), + }), + most_recent_commit: branch + .most_recent_commit + .as_ref() + .map(|commit| proto::CommitSummary { + sha: commit.sha.to_string(), + subject: commit.subject.to_string(), + commit_timestamp: commit.commit_timestamp, + }), + } +} + +pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch { + git::repository::Branch { + is_head: proto.is_head, + name: proto.name.clone().into(), + upstream: proto + .upstream + .as_ref() + .map(|upstream| git::repository::Upstream { + ref_name: upstream.ref_name.to_string().into(), + tracking: upstream.tracking.as_ref().map(|tracking| { + git::repository::UpstreamTracking { + ahead: tracking.ahead as u32, + behind: tracking.behind as u32, + } + }), + }), + most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| { + git::repository::CommitSummary { + sha: commit.sha.to_string().into(), + subject: commit.subject.to_string().into(), + commit_timestamp: commit.commit_timestamp, + } + }), + } +} + /// This path corresponds to the 'content path' of a repository in relation /// to Zed's project root. /// In the majority of the cases, this is the folder that contains the .git folder. @@ -2625,7 +2682,7 @@ impl Snapshot { self.repositories .update(&PathKey(work_dir_entry.path.clone()), &(), |repo| { - repo.branch = repository.branch.map(Into::into); + repo.branch = repository.branch_summary.as_ref().map(proto_to_branch); repo.statuses_by_path.edit(edits, &()); repo.current_merge_conflicts = conflicted_paths }); @@ -2647,7 +2704,7 @@ impl Snapshot { work_directory: WorkDirectory::InProject { relative_path: work_dir_entry.path.clone(), }, - branch: repository.branch.map(Into::into), + branch: repository.branch_summary.as_ref().map(proto_to_branch), statuses_by_path: statuses, current_merge_conflicts: conflicted_paths, }, @@ -3449,7 +3506,7 @@ impl BackgroundScannerState { RepositoryEntry { work_directory_id: work_dir_id, work_directory: work_directory.clone(), - branch: repository.branch_name().map(Into::into), + branch: None, statuses_by_path: Default::default(), current_merge_conflicts: Default::default(), }, @@ -4198,6 +4255,7 @@ impl BackgroundScanner { // the git repository in an ancestor directory. Find any gitignore files // in ancestor directories. let root_abs_path = self.state.lock().snapshot.abs_path.clone(); + let mut containing_git_repository = None; for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() { if index != 0 { if let Ok(ignore) = @@ -4227,7 +4285,7 @@ impl BackgroundScanner { { // We associate the external git repo with our root folder and // also mark where in the git repo the root folder is located. - self.state.lock().insert_git_repository_for_path( + let local_repository = self.state.lock().insert_git_repository_for_path( WorkDirectory::AboveProject { absolute_path: ancestor.into(), location_in_repo: root_abs_path @@ -4236,10 +4294,14 @@ impl BackgroundScanner { .unwrap() .into(), }, - ancestor_dot_git.into(), + ancestor_dot_git.clone().into(), self.fs.as_ref(), self.watcher.as_ref(), ); + + if local_repository.is_some() { + containing_git_repository = Some(ancestor_dot_git) + } }; } @@ -4285,6 +4347,9 @@ impl BackgroundScanner { self.process_events(paths.into_iter().map(Into::into).collect()) .await; } + if let Some(abs_path) = containing_git_repository { + self.process_events(vec![abs_path]).await; + } // Continue processing events until the worktree is dropped. self.phase = BackgroundScannerPhase::Events; @@ -4703,7 +4768,7 @@ impl BackgroundScanner { ); if let Some(local_repo) = repo { - self.update_git_statuses(UpdateGitStatusesJob { + self.update_git_repository(UpdateGitRepoJob { local_repository: local_repo, }); } @@ -5255,15 +5320,6 @@ impl BackgroundScanner { if local_repository.git_dir_scan_id == scan_id { continue; } - let Some(work_dir) = state - .snapshot - .entry_for_id(local_repository.work_directory_id) - .map(|entry| entry.path.clone()) - else { - continue; - }; - - let branch = local_repository.repo_ptr.branch_name(); local_repository.repo_ptr.reload_index(); state.snapshot.git_repositories.update( @@ -5273,17 +5329,12 @@ impl BackgroundScanner { entry.status_scan_id = scan_id; }, ); - state.snapshot.snapshot.repositories.update( - &PathKey(work_dir.clone()), - &(), - |entry| entry.branch = branch.map(Into::into), - ); local_repository } }; - repo_updates.push(UpdateGitStatusesJob { local_repository }); + repo_updates.push(UpdateGitRepoJob { local_repository }); } // Remove any git repositories whose .git entry no longer exists. @@ -5319,7 +5370,7 @@ impl BackgroundScanner { .scoped(|scope| { scope.spawn(async { for repo_update in repo_updates { - self.update_git_statuses(repo_update); + self.update_git_repository(repo_update); } updates_done_tx.blocking_send(()).ok(); }); @@ -5343,22 +5394,37 @@ impl BackgroundScanner { .await; } - /// Update the git statuses for a given batch of entries. - fn update_git_statuses(&self, job: UpdateGitStatusesJob) { + fn update_branches(&self, job: &UpdateGitRepoJob) -> Result<()> { + let branches = job.local_repository.repo().branches()?; + let snapshot = self.state.lock().snapshot.snapshot.clone(); + + let mut repository = snapshot + .repository(job.local_repository.work_directory.path_key()) + .context("Missing repository")?; + + repository.branch = branches.into_iter().find(|branch| branch.is_head); + + let mut state = self.state.lock(); + state + .snapshot + .repositories + .insert_or_replace(repository, &()); + + Ok(()) + } + + fn update_statuses(&self, job: &UpdateGitRepoJob) -> Result<()> { log::trace!( "updating git statuses for repo {:?}", job.local_repository.work_directory.display_name() ); let t0 = Instant::now(); - let Some(statuses) = job + let statuses = job .local_repository .repo() - .status(&[git::WORK_DIRECTORY_REPO_PATH.clone()]) - .log_err() - else { - return; - }; + .status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])?; + log::trace!( "computed git statuses for repo {:?} in {:?}", job.local_repository.work_directory.display_name(), @@ -5369,13 +5435,9 @@ impl BackgroundScanner { let mut changed_paths = Vec::new(); let snapshot = self.state.lock().snapshot.snapshot.clone(); - let Some(mut repository) = - snapshot.repository(job.local_repository.work_directory.path_key()) - else { - // happens when a folder is deleted - log::debug!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot"); - return; - }; + let mut repository = snapshot + .repository(job.local_repository.work_directory.path_key()) + .context("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot")?; let merge_head_shas = job.local_repository.repo().merge_head_shas(); if merge_head_shas != job.local_repository.current_merge_head_shas { @@ -5403,6 +5465,7 @@ impl BackgroundScanner { } repository.statuses_by_path = new_entries_by_path; + let mut state = self.state.lock(); state .snapshot @@ -5428,6 +5491,13 @@ impl BackgroundScanner { job.local_repository.work_directory.display_name(), t0.elapsed(), ); + Ok(()) + } + + /// Update the git statuses for a given batch of entries. + fn update_git_repository(&self, job: UpdateGitRepoJob) { + self.update_branches(&job).log_err(); + self.update_statuses(&job).log_err(); } fn build_change_set( @@ -5637,7 +5707,7 @@ struct UpdateIgnoreStatusJob { scan_queue: Sender, } -struct UpdateGitStatusesJob { +struct UpdateGitRepoJob { local_repository: LocalRepositoryEntry, }