Cargo.lock 🔗
@@ -5371,6 +5371,8 @@ dependencies = [
"serde_json",
"settings",
"theme",
+ "time",
+ "time_format",
"ui",
"util",
"windows 0.58.0",
Mikayla Maki , Conrad Irwin , Conrad , and Nate Butler created
Also prep infrastructure for pushing a commit
Release Notes:
- N/A
---------
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Cargo.lock | 2
assets/keymaps/vim.json | 2
crates/collab/migrations.sqlite/20221109000000_test_schema.sql | 1
crates/collab/migrations/20250210223746_add_branch_summary.sql | 2
crates/collab/migrations/20250212060936_add_worktree_branch_summary.sql | 1
crates/collab/src/db/queries/projects.rs | 40
crates/collab/src/db/queries/rooms.rs | 8
crates/collab/src/db/tables/worktree_repository.rs | 2
crates/collab/src/rpc.rs | 2
crates/collab/src/tests/integration_tests.rs | 9
crates/collab/src/tests/remote_editing_collaboration_tests.rs | 4
crates/editor/src/commit_tooltip.rs | 143
crates/editor/src/editor.rs | 2
crates/editor/src/element.rs | 19
crates/editor/src/git/blame.rs | 31
crates/git/src/blame.rs | 16
crates/git/src/git.rs | 1
crates/git/src/hosting_provider.rs | 6
crates/git/src/repository.rs | 320
crates/git/test_data/golden/blame_incremental_complex.json | 164
crates/git/test_data/golden/blame_incremental_not_committed.json | 28
crates/git/test_data/golden/blame_incremental_simple.json | 28
crates/git_hosting_providers/src/providers/codeberg.rs | 5
crates/git_hosting_providers/src/providers/github.rs | 5
crates/git_ui/Cargo.toml | 2
crates/git_ui/src/branch_picker.rs | 23
crates/git_ui/src/git_panel.rs | 265
crates/git_ui/src/project_diff.rs | 20
crates/git_ui/src/quick_commit.rs | 4
crates/git_ui/src/repository_selector.rs | 12
crates/project/src/buffer_store.rs | 8
crates/project/src/git.rs | 358
crates/project/src/project.rs | 181
crates/project/src/worktree_store.rs | 39
crates/proto/proto/zed.proto | 50
crates/proto/src/proto.rs | 7
crates/remote_server/src/headless_project.rs | 178
crates/remote_server/src/remote_editing_tests.rs | 4
crates/time_format/src/time_format.rs | 24
crates/title_bar/src/title_bar.rs | 1
crates/worktree/src/worktree.rs | 158
41 files changed, 1,437 insertions(+), 738 deletions(-)
@@ -5371,6 +5371,8 @@ dependencies = [
"serde_json",
"settings",
"theme",
+ "time",
+ "time_format",
"ui",
"util",
"windows 0.58.0",
@@ -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"
@@ -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
@@ -0,0 +1,2 @@
+ALTER TABLE worktree_repositories
+ADD COLUMN worktree_repositories VARCHAR NULL;
@@ -0,0 +1 @@
+ALTER TABLE worktree_repositories ADD COLUMN branch_summary TEXT NULL;
@@ -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,
},
);
}
@@ -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,
});
}
}
@@ -15,6 +15,8 @@ pub struct Model {
pub is_deleted: bool,
// JSON array typed string
pub current_merge_conflicts: Option<String>,
+ // A JSON object representing the current Branch values
+ pub branch_summary: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -395,6 +395,8 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
+ .add_request_handler(forward_read_only_project_request::<proto::GitShow>)
+ .add_request_handler(forward_read_only_project_request::<proto::GitReset>)
.add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
.add_request_handler(forward_mutating_project_request::<proto::OpenCommitMessageBuffer>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
@@ -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");
}
@@ -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]
@@ -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<ParsedCommitMessage>,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ParsedCommitMessage {
+ pub message: SharedString,
+ pub parsed_message: ParsedMarkdown,
+ pub permalink: Option<Url>,
+ pub pull_request: Option<PullRequest>,
+ pub remote: Option<GitRemote>,
+}
+
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<BlameEntryTooltip>,
+ cx: &mut Context<CommitTooltip>,
) -> Option<impl IntoElement> {
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::<CommitAvatarAsset>(&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<CommitDetails>,
+pub struct CommitTooltip {
+ commit: CommitDetails,
editor_style: EditorStyle,
workspace: Option<WeakEntity<Workspace>>,
scroll_handle: ScrollHandle,
}
-impl BlameEntryTooltip {
- pub(crate) fn new(
- blame_entry: BlameEntry,
- details: Option<CommitDetails>,
- style: &EditorStyle,
+impl CommitTooltip {
+ pub fn blame_entry(
+ blame: BlameEntry,
+ details: Option<ParsedCommitMessage>,
+ style: EditorStyle,
+ workspace: Option<WeakEntity<Workspace>>,
+ ) -> 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("<no name>".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<WeakEntity<Workspace>>,
) -> 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<Self>) -> 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("<no name>".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("<no commit message>".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)
-}
@@ -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;
@@ -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<Editor>,
position: gpui::Point<Pixels>,
window: &mut Window,
@@ -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<dyn HttpClient>) -> Option<Url> {
+ pub async fn avatar_url(
+ &self,
+ commit: SharedString,
+ client: Arc<dyn HttpClient>,
+ ) -> Option<Url> {
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<Url>,
- pub pull_request: Option<PullRequest>,
- pub remote: Option<GitRemote>,
-}
-
pub struct GitBlame {
project: Entity<Project>,
buffer: Entity<Buffer>,
entries: SumTree<GitBlameEntry>,
- commit_details: HashMap<Oid, CommitDetails>,
+ commit_details: HashMap<Oid, crate::commit_tooltip::ParsedCommitMessage>,
buffer_snapshot: BufferSnapshot,
buffer_edits: text::Subscription,
task: Task<Result<()>>,
@@ -187,7 +184,7 @@ impl GitBlame {
self.generated
}
- pub fn details_for_entry(&self, entry: &BlameEntry) -> Option<CommitDetails> {
+ pub fn details_for_entry(&self, entry: &BlameEntry) -> Option<ParsedCommitMessage> {
self.commit_details.get(&entry.sha).cloned()
}
@@ -480,7 +477,7 @@ async fn parse_commit_messages(
deprecated_permalinks: &HashMap<Oid, Url>,
provider_registry: Arc<GitHostingProviderRegistry>,
languages: &Arc<LanguageRegistry>,
-) -> HashMap<Oid, CommitDetails> {
+) -> HashMap<Oid, ParsedCommitMessage> {
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,
@@ -132,8 +132,8 @@ pub struct BlameEntry {
pub author_time: Option<i64>,
pub author_tz: Option<String>,
- pub committer: Option<String>,
- pub committer_mail: Option<String>,
+ pub committer_name: Option<String>,
+ pub committer_email: Option<String>,
pub committer_time: Option<i64>,
pub committer_tz: Option<String>,
@@ -255,10 +255,12 @@ fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
.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<Vec<BlameEntry>> {
}
"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::<i64>()?)
}
@@ -38,6 +38,7 @@ actions!(
StageAll,
UnstageAll,
RevertAll,
+ Uncommit,
Commit,
ClearCommitMessage
]
@@ -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<dyn HttpClient>,
) -> Result<Option<Url>> {
Ok(None)
@@ -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<i64>,
+ pub upstream: Option<Upstream>,
+ pub most_recent_commit: Option<CommitSummary>,
+}
+
+impl Branch {
+ pub fn priority_key(&self) -> (bool, Option<i64>) {
+ (
+ 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<UpstreamTracking>,
+}
+
+#[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<String>;
- fn branch_name(&self) -> Option<String>;
/// Returns the SHA of the current HEAD.
fn head_sha(&self) -> Option<String>;
@@ -60,6 +110,10 @@ pub trait GitRepository: Send + Sync {
fn create_branch(&self, _: &str) -> Result<()>;
fn branch_exits(&self, _: &str) -> Result<bool>;
+ fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
+
+ fn show(&self, commit: &str) -> Result<CommitDetails>;
+
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
/// 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<CommitDetails> {
+ 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<String> {
fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
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<String> {
- 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<String> {
Some(self.repository.lock().head().ok()?.target()?.to_string())
}
@@ -261,33 +355,62 @@ impl GitRepository for RealGitRepository {
}
fn branches(&self) -> Result<Vec<Branch>> {
- 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<String> {
- let state = self.state.lock();
- state.current_branch_name.clone()
- }
-
fn head_sha(&self) -> Option<String> {
None
}
@@ -491,6 +609,14 @@ impl GitRepository for FakeGitRepository {
vec![]
}
+ fn show(&self, _: &str) -> Result<CommitDetails> {
+ 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<RepoPath> for RepoPathDescendants<'a> {
}
}
}
+
+fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
+ 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::<i64>()?;
+ 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<Option<UpstreamTracking>> {
+ 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::<u32>()?;
+ }
+ if let Some(behind_num) = component.strip_prefix("behind ") {
+ behind = behind_num.parse::<u32>()?;
+ }
+ }
+ 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,
+ })
+ }]
+ )
+}
@@ -10,8 +10,8 @@
"author_mail": "<64036912+mmkaram@users.noreply.github.com>",
"author_time": 1708621949,
"author_tz": "-0800",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"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": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"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": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"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": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"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": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"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": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"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": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1708621949,
"committer_tz": "-0700",
"summary": "Add option to either use system clipboard or vim clipboard (#7936)",
@@ -143,8 +143,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -162,8 +162,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -181,8 +181,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -200,8 +200,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -219,8 +219,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -238,8 +238,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -257,8 +257,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -276,8 +276,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -295,8 +295,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -314,8 +314,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -333,8 +333,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1707520689,
"author_tz": "-0700",
- "committer": "GitHub",
- "committer_mail": "<noreply@github.com>",
+ "committer_name": "GitHub",
+ "committer_email": "<noreply@github.com>",
"committer_time": 1707520689,
"committer_tz": "-0700",
"summary": "Highlight selections on vim yank (#7638)",
@@ -352,8 +352,8 @@
"author_mail": "<maxbrunsfeld@gmail.com>",
"author_time": 1705619094,
"author_tz": "-0800",
- "committer": "Max Brunsfeld",
- "committer_mail": "<maxbrunsfeld@gmail.com>",
+ "committer_name": "Max Brunsfeld",
+ "committer_email": "<maxbrunsfeld@gmail.com>",
"committer_time": 1705619205,
"committer_tz": "-0800",
"summary": "Merge branch 'main' into language-api-docs",
@@ -371,8 +371,8 @@
"author_mail": "<maxbrunsfeld@gmail.com>",
"author_time": 1705619094,
"author_tz": "-0800",
- "committer": "Max Brunsfeld",
- "committer_mail": "<maxbrunsfeld@gmail.com>",
+ "committer_name": "Max Brunsfeld",
+ "committer_email": "<maxbrunsfeld@gmail.com>",
"committer_time": 1705619205,
"committer_tz": "-0800",
"summary": "Merge branch 'main' into language-api-docs",
@@ -390,8 +390,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1694798044,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1694798044,
"committer_tz": "-0600",
"summary": "Fix Y on last line with no trailing new line",
@@ -409,8 +409,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1694798044,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1694798044,
"committer_tz": "-0600",
"summary": "Fix Y on last line with no trailing new line",
@@ -428,8 +428,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1692855942,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1692856812,
"committer_tz": "-0600",
"summary": "vim: Fix linewise copy of last line with no trailing newline",
@@ -447,8 +447,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1692855942,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1692856812,
"committer_tz": "-0600",
"summary": "vim: Fix linewise copy of last line with no trailing newline",
@@ -466,8 +466,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1692855942,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1692856812,
"committer_tz": "-0600",
"summary": "vim: Fix linewise copy of last line with no trailing newline",
@@ -485,8 +485,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1692855942,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1692856812,
"committer_tz": "-0600",
"summary": "vim: Fix linewise copy of last line with no trailing newline",
@@ -504,8 +504,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1692855942,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1692856812,
"committer_tz": "-0600",
"summary": "vim: Fix linewise copy of last line with no trailing newline",
@@ -523,8 +523,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1692644159,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1692732477,
"committer_tz": "-0600",
"summary": "Rewrite paste",
@@ -542,8 +542,8 @@
"author_mail": "<conrad@zed.dev>",
"author_time": 1692644159,
"author_tz": "-0600",
- "committer": "Conrad Irwin",
- "committer_mail": "<conrad@zed.dev>",
+ "committer_name": "Conrad Irwin",
+ "committer_email": "<conrad@zed.dev>",
"committer_time": 1692732477,
"committer_tz": "-0600",
"summary": "Rewrite paste",
@@ -561,8 +561,8 @@
"author_mail": "<maxbrunsfeld@gmail.com>",
"author_time": 1659072896,
"author_tz": "-0700",
- "committer": "Max Brunsfeld",
- "committer_mail": "<maxbrunsfeld@gmail.com>",
+ "committer_name": "Max Brunsfeld",
+ "committer_email": "<maxbrunsfeld@gmail.com>",
"committer_time": 1659073230,
"committer_tz": "-0700",
"summary": ":art: Rename and simplify some autoindent stuff",
@@ -580,8 +580,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653424557,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Unify visual line_mode and non line_mode operators",
@@ -599,8 +599,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -618,8 +618,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -637,8 +637,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -656,8 +656,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -675,8 +675,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -694,8 +694,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -713,8 +713,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -732,8 +732,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -751,8 +751,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -770,8 +770,8 @@
"author_mail": "<kay@the-simmons.net>",
"author_time": 1653007350,
"author_tz": "-0700",
- "committer": "Kaylee Simmons",
- "committer_mail": "<kay@the-simmons.net>",
+ "committer_name": "Kaylee Simmons",
+ "committer_email": "<kay@the-simmons.net>",
"committer_time": 1653609725,
"committer_tz": "-0700",
"summary": "Enable copy and paste in vim mode",
@@ -10,8 +10,8 @@
"author_mail": "<mrnugget@gmail.com>",
"author_time": 1710764113,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@gmail.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@gmail.com>",
"committer_time": 1710764113,
"committer_tz": "+0100",
"summary": "Another commit",
@@ -29,8 +29,8 @@
"author_mail": "<mrnugget@gmail.com>",
"author_time": 1710764113,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@gmail.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@gmail.com>",
"committer_time": 1710764113,
"committer_tz": "+0100",
"summary": "Another commit",
@@ -48,8 +48,8 @@
"author_mail": "<mrnugget@gmail.com>",
"author_time": 1710764087,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@gmail.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@gmail.com>",
"committer_time": 1710764087,
"committer_tz": "+0100",
"summary": "Another commit",
@@ -67,8 +67,8 @@
"author_mail": "<mrnugget@gmail.com>",
"author_time": 1710764087,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@gmail.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@gmail.com>",
"committer_time": 1710764087,
"committer_tz": "+0100",
"summary": "Another commit",
@@ -86,8 +86,8 @@
"author_mail": "<mrnugget@gmail.com>",
"author_time": 1709299737,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@gmail.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@gmail.com>",
"committer_time": 1709299737,
"committer_tz": "+0100",
"summary": "Initial",
@@ -105,8 +105,8 @@
"author_mail": "<mrnugget@gmail.com>",
"author_time": 1709299737,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@gmail.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@gmail.com>",
"committer_time": 1709299737,
"committer_tz": "+0100",
"summary": "Initial",
@@ -124,8 +124,8 @@
"author_mail": "<mrnugget@gmail.com>",
"author_time": 1709299737,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@gmail.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@gmail.com>",
"committer_time": 1709299737,
"committer_tz": "+0100",
"summary": "Initial",
@@ -10,8 +10,8 @@
"author_mail": "<mrnugget@example.com>",
"author_time": 1709808710,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@example.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@example.com>",
"committer_time": 1709808710,
"committer_tz": "+0100",
"summary": "Make a commit",
@@ -29,8 +29,8 @@
"author_mail": "<joe.schmoe@example.com>",
"author_time": 1709741400,
"author_tz": "+0100",
- "committer": "Joe Schmoe",
- "committer_mail": "<joe.schmoe@example.com>",
+ "committer_name": "Joe Schmoe",
+ "committer_email": "<joe.schmoe@example.com>",
"committer_time": 1709741400,
"committer_tz": "+0100",
"summary": "Joe's cool commit",
@@ -48,8 +48,8 @@
"author_mail": "<joe.schmoe@example.com>",
"author_time": 1709741400,
"author_tz": "+0100",
- "committer": "Joe Schmoe",
- "committer_mail": "<joe.schmoe@example.com>",
+ "committer_name": "Joe Schmoe",
+ "committer_email": "<joe.schmoe@example.com>",
"committer_time": 1709741400,
"committer_tz": "+0100",
"summary": "Joe's cool commit",
@@ -67,8 +67,8 @@
"author_mail": "<joe.schmoe@example.com>",
"author_time": 1709741400,
"author_tz": "+0100",
- "committer": "Joe Schmoe",
- "committer_mail": "<joe.schmoe@example.com>",
+ "committer_name": "Joe Schmoe",
+ "committer_email": "<joe.schmoe@example.com>",
"committer_time": 1709741400,
"committer_tz": "+0100",
"summary": "Joe's cool commit",
@@ -86,8 +86,8 @@
"author_mail": "<mrnugget@example.com>",
"author_time": 1709129122,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@example.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@example.com>",
"committer_time": 1709129122,
"committer_tz": "+0100",
"summary": "Get to a state where eslint would change code and imports",
@@ -105,8 +105,8 @@
"author_mail": "<mrnugget@example.com>",
"author_time": 1709128963,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@example.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@example.com>",
"committer_time": 1709128963,
"committer_tz": "+0100",
"summary": "Add some stuff",
@@ -124,8 +124,8 @@
"author_mail": "<mrnugget@example.com>",
"author_time": 1709128963,
"author_tz": "+0100",
- "committer": "Thorsten Ball",
- "committer_mail": "<mrnugget@example.com>",
+ "committer_name": "Thorsten Ball",
+ "committer_email": "<mrnugget@example.com>",
"committer_time": 1709128963,
"committer_tz": "+0100",
"summary": "Add some stuff",
@@ -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<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
@@ -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<dyn HttpClient>,
) -> Result<Option<Url>> {
let commit = commit.to_string();
@@ -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
@@ -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 {
@@ -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<Self> {
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<Self>) {
+ 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<Self>) {
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<Self>) -> Option<impl IntoElement> {
+ 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<Self>) -> 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<Self>,
+ ) -> Task<Result<CommitDetails>> {
+ 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<Entity<CommitTooltip>>,
+}
+
+impl GitPanelMessageTooltip {
+ fn new(
+ git_panel: Entity<GitPanel>,
+ sha: SharedString,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Entity<Self> {
+ 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()
+ }
+ }
+}
@@ -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<Editor>,
project: Entity<Project>,
git_panel: Entity<GitPanel>,
- git_state: Entity<GitState>,
+ git_store: Entity<GitStore>,
workspace: WeakEntity<Workspace>,
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<Self>,
) {
- 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<Self>) -> Vec<Task<Result<DiffBuffer>>> {
- 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);
});
@@ -98,7 +98,7 @@ impl QuickCommitModal {
commit_message_buffer: Option<Entity<Buffer>>,
cx: &mut Context<Self>,
) -> 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
@@ -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<Project>, window: &mut Window, cx: &mut Context<Self>) -> 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<GitState>,
+ git_store: &Entity<GitStore>,
_event: &project::git::GitEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -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);
@@ -2722,8 +2722,8 @@ fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> 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,
@@ -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<ProjectId>,
pub(super) client: Option<AnyProtoClient>,
- pub update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
+ buffer_store: Entity<BufferStore>,
repositories: Vec<Entity<Repository>>,
active_index: Option<usize>,
+ update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)>,
_subscription: Subscription,
}
pub struct Repository {
commit_message_buffer: Option<Entity<Buffer>>,
- git_state: WeakEntity<GitState>,
+ git_store: WeakEntity<GitStore>,
pub worktree_id: WorktreeId,
pub repository_entry: RepositoryEntry,
pub git_repo: GitRepo,
- update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<anyhow::Result<()>>)>,
+ update_sender: mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)>,
}
#[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<RepoPath>),
Unstage(GitRepo, Vec<RepoPath>),
SetIndexText(GitRepo, RepoPath, Option<String>),
@@ -68,11 +76,12 @@ pub enum GitEvent {
GitStateUpdated,
}
-impl EventEmitter<GitEvent> for GitState {}
+impl EventEmitter<GitEvent> for GitStore {}
-impl GitState {
+impl GitStore {
pub fn new(
worktree_store: &Entity<WorktreeStore>,
+ buffer_store: Entity<BufferStore>,
client: Option<AnyProtoClient>,
project_id: Option<ProjectId>,
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<Entity<Repository>> {
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<anyhow::Result<()>>)> {
+ cx: &mut Context<'_, GitStore>,
+ ) -> mpsc::UnboundedSender<(Message, oneshot::Sender<Result<()>>)> {
let (update_sender, mut update_receiver) =
- mpsc::unbounded::<(Message, oneshot::Sender<anyhow::Result<()>>)>();
+ mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
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<Self>,
+ envelope: TypedEnvelope<proto::Stage>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::Unstage>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::SetIndexText>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::Commit>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::GitShow>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::GitCommitDetails> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::GitReset>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::Ack> {
+ 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<Self>,
+ envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::OpenBufferResponse> {
+ 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<Self>,
+ worktree_id: WorktreeId,
+ work_directory_id: ProjectEntryId,
+ cx: &mut AsyncApp,
+ ) -> Result<Entity<Repository>> {
+ 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<Entity<GitState>> {
- self.git_state.upgrade()
+ pub fn git_store(&self) -> Option<Entity<GitStore>> {
+ self.git_store.upgrade()
}
fn id(&self) -> (WorktreeId, ProjectEntryId) {
(self.worktree_id, self.repository_entry.work_directory_id())
}
- pub fn branch(&self) -> Option<Arc<str>> {
+ pub fn branch(&self) -> Option<&Branch> {
self.repository_entry.branch()
}
@@ -344,19 +592,19 @@ impl Repository {
}
pub fn activate(&self, cx: &mut Context<Self>) {
- 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<Arc<LanguageRegistry>>,
buffer_store: Entity<BufferStore>,
cx: &mut Context<Self>,
- ) -> Task<anyhow::Result<Entity<Buffer>>> {
+ ) -> Task<Result<Entity<Buffer>>> {
if let Some(buffer) = self.commit_message_buffer.clone() {
return Task::ready(Ok(buffer));
}
@@ -444,7 +692,7 @@ impl Repository {
language_registry: Option<Arc<LanguageRegistry>>,
buffer_store: Entity<BufferStore>,
cx: &mut Context<Self>,
- ) -> Task<anyhow::Result<Entity<Buffer>>> {
+ ) -> Task<Result<Entity<Buffer>>> {
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<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
+ pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
+ 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<Self>) -> Task<Result<CommitDetails>> {
+ 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<RepoPath>) -> oneshot::Receiver<Result<()>> {
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<RepoPath>) -> oneshot::Receiver<anyhow::Result<()>> {
+ pub fn unstage_entries(&self, entries: Vec<RepoPath>) -> oneshot::Receiver<Result<()>> {
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<anyhow::Result<()>> {
+ pub fn stage_all(&self) -> oneshot::Receiver<Result<()>> {
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<anyhow::Result<()>> {
+ pub fn unstage_all(&self) -> oneshot::Receiver<Result<()>> {
let to_unstage = self
.repository_entry
.status()
@@ -530,7 +828,7 @@ impl Repository {
&self,
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
- ) -> oneshot::Receiver<anyhow::Result<()>> {
+ ) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender
.unbounded_send((
@@ -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<dyn Fs>,
ssh_client: Option<Entity<SshRemoteClient>>,
client_state: ProjectClientState,
- git_state: Entity<GitState>,
+ git_store: Entity<GitStore>,
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
worktree_store: Entity<WorktreeStore>,
@@ -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<Self>,
- envelope: TypedEnvelope<proto::Stage>,
- mut cx: AsyncApp,
- ) -> Result<proto::Ack> {
- 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<Self>,
- envelope: TypedEnvelope<proto::Unstage>,
- mut cx: AsyncApp,
- ) -> Result<proto::Ack> {
- 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<Self>,
- envelope: TypedEnvelope<proto::Commit>,
- mut cx: AsyncApp,
- ) -> Result<proto::Ack> {
- 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<Self>,
- envelope: TypedEnvelope<proto::SetIndexText>,
- mut cx: AsyncApp,
- ) -> Result<proto::Ack> {
- 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<Self>,
- envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
- mut cx: AsyncApp,
- ) -> Result<proto::OpenBufferResponse> {
- 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<Self>,
- worktree_id: WorktreeId,
- work_directory_id: ProjectEntryId,
- cx: &mut AsyncApp,
- ) -> Result<Entity<Repository>> {
- 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<Self>,
buffer: Entity<Buffer>,
@@ -4365,16 +4232,16 @@ impl Project {
&self.buffer_store
}
- pub fn git_state(&self) -> &Entity<GitState> {
- &self.git_state
+ pub fn git_store(&self) -> &Entity<GitStore> {
+ &self.git_store
}
pub fn active_repository(&self, cx: &App) -> Option<Entity<Repository>> {
- self.git_state.read(cx).active_repository()
+ self.git_store.read(cx).active_repository()
}
pub fn all_repositories(&self, cx: &App) -> Vec<Entity<Repository>> {
- 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()
@@ -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<Arc<str>> {
+ pub fn current_branch(&self, repository: ProjectPath, cx: &App) -> Option<Branch> {
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(),
})
}
@@ -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;
}
@@ -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,
);
@@ -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<AtomicUsize>,
pub languages: Arc<LanguageRegistry>,
pub extensions: Entity<HeadlessExtensionStore>,
- pub git_state: Entity<GitState>,
+ pub git_store: Entity<GitStore>,
}
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<Self>,
- envelope: TypedEnvelope<proto::Stage>,
- mut cx: AsyncApp,
- ) -> Result<proto::Ack> {
- 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<Self>,
- envelope: TypedEnvelope<proto::Unstage>,
- mut cx: AsyncApp,
- ) -> Result<proto::Ack> {
- 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<Self>,
- envelope: TypedEnvelope<proto::Commit>,
- mut cx: AsyncApp,
- ) -> Result<proto::Ack> {
- 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<Self>,
- envelope: TypedEnvelope<proto::SetIndexText>,
- mut cx: AsyncApp,
- ) -> Result<proto::Ack> {
- 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<Self>,
- envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
- mut cx: AsyncApp,
- ) -> Result<proto::OpenBufferResponse> {
- 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<Self>,
- worktree_id: WorktreeId,
- work_directory_id: ProjectEntryId,
- cx: &mut AsyncApp,
- ) -> Result<Entity<Repository>> {
- 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(
@@ -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(
@@ -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)),
}
}
@@ -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)
@@ -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<StatusEntry>,
work_directory_id: ProjectEntryId,
pub work_directory: WorkDirectory,
- pub(crate) branch: Option<Arc<str>>,
+ pub(crate) branch: Option<Branch>,
pub current_merge_conflicts: TreeSet<RepoPath>,
}
@@ -214,8 +214,8 @@ impl Deref for RepositoryEntry {
}
impl RepositoryEntry {
- pub fn branch(&self) -> Option<Arc<str>> {
- 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<ScanJob>,
}
-struct UpdateGitStatusesJob {
+struct UpdateGitRepoJob {
local_repository: LocalRepositoryEntry,
}