This commit is contained in:
Alvaro Parker 2025-08-08 19:32:11 -04:00
parent 4bee06e507
commit 231a348e69
No known key found for this signature in database
6 changed files with 106 additions and 1 deletions

View file

@ -320,6 +320,10 @@ impl GitRepository for FakeGitRepository {
}) })
} }
fn stash_entries(&self) -> BoxFuture<'_, Result<git::stash::GitStash>> {
unimplemented!()
}
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> { fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
self.with_state_async(false, move |state| { self.with_state_async(false, move |state| {
let current_branch = &state.current_branch_name; let current_branch = &state.current_branch_name;

View file

@ -3,6 +3,7 @@ pub mod commit;
mod hosting_provider; mod hosting_provider;
mod remote; mod remote;
pub mod repository; pub mod repository;
pub mod stash;
pub mod status; pub mod status;
pub use crate::hosting_provider::*; pub use crate::hosting_provider::*;

View file

@ -1,4 +1,5 @@
use crate::commit::parse_git_diff_name_status; use crate::commit::parse_git_diff_name_status;
use crate::stash::GitStash;
use crate::status::{GitStatus, StatusCode}; use crate::status::{GitStatus, StatusCode};
use crate::{Oid, SHORT_SHA_LENGTH}; use crate::{Oid, SHORT_SHA_LENGTH};
use anyhow::{Context as _, Result, anyhow, bail}; use anyhow::{Context as _, Result, anyhow, bail};
@ -338,6 +339,8 @@ pub trait GitRepository: Send + Sync {
fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>>; fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>>;
fn stash_entries(&self) -> BoxFuture<'_, Result<GitStash>>;
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>>; fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>>;
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>; fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
@ -974,6 +977,26 @@ impl GitRepository for RealGitRepository {
}) })
} }
fn stash_entries(&self) -> BoxFuture<'_, Result<GitStash>> {
let git_binary_path = self.git_binary_path.clone();
let working_directory = self.working_directory();
self.executor
.spawn(async move {
let output = new_std_command(&git_binary_path)
.current_dir(working_directory?)
.args(&["stash", "list", "--pretty=%gd:%H:%s"])
.output()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.parse()
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git status failed: {stderr}");
}
})
.boxed()
}
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> { fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
let working_directory = self.working_directory(); let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone(); let git_binary_path = self.git_binary_path.clone();

56
crates/git/src/stash.rs Normal file
View file

@ -0,0 +1,56 @@
use crate::Oid;
use anyhow::Result;
use std::{str::FromStr, sync::Arc};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct StashEntry {
pub index: usize,
pub oid: Oid,
pub message: String,
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct GitStash {
pub entries: Arc<[StashEntry]>,
}
impl FromStr for GitStash {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
// git stash list --pretty=%gd:%H:%s
let entries = s
.split('\n')
.filter_map(|entry| {
let mut parts = entry.splitn(3, ':');
let raw_idx = parts.next().and_then(|i| {
let trimmed = i.trim();
if trimmed.starts_with("stash@{") && trimmed.ends_with('}') {
trimmed
.strip_prefix("stash@{")
.and_then(|s| s.strip_suffix('}'))
} else {
None
}
});
let raw_oid = parts.next();
let message = parts.next();
if let (Some(raw_idx), Some(raw_oid), Some(message)) = (raw_idx, raw_oid, message) {
let index = raw_idx.parse::<usize>().ok()?;
let oid = Oid::from_str(raw_oid).ok()?;
let entry = StashEntry {
index,
oid,
message: message.to_string(),
};
return Some(entry);
}
None
})
.collect::<Arc<[StashEntry]>>();
Ok(Self {
entries: entries.clone(),
})
}
}

View file

@ -24,6 +24,7 @@ use git::repository::{
PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
UpstreamTrackingStatus, get_git_committer, UpstreamTrackingStatus, get_git_committer,
}; };
use git::stash::GitStash;
use git::status::StageStatus; use git::status::StageStatus;
use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus}; use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
use git::{ use git::{
@ -119,6 +120,7 @@ struct GitMenuState {
has_staged_changes: bool, has_staged_changes: bool,
has_unstaged_changes: bool, has_unstaged_changes: bool,
has_new_changes: bool, has_new_changes: bool,
has_stash_items: bool,
} }
fn git_panel_context_menu( fn git_panel_context_menu(
@ -146,7 +148,7 @@ fn git_panel_context_menu(
"Stash All", "Stash All",
StashAll.boxed_clone(), StashAll.boxed_clone(),
) )
.action("Stash Pop", StashPop.boxed_clone()) .action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone())
.separator() .separator()
.action("Open Diff", project_diff::Diff.boxed_clone()) .action("Open Diff", project_diff::Diff.boxed_clone())
.separator() .separator()
@ -369,6 +371,7 @@ pub struct GitPanel {
local_committer: Option<GitCommitter>, local_committer: Option<GitCommitter>,
local_committer_task: Option<Task<()>>, local_committer_task: Option<Task<()>>,
bulk_staging: Option<BulkStaging>, bulk_staging: Option<BulkStaging>,
stash_entries: GitStash,
_settings_subscription: Subscription, _settings_subscription: Subscription,
} }
@ -558,6 +561,7 @@ impl GitPanel {
horizontal_scrollbar, horizontal_scrollbar,
vertical_scrollbar, vertical_scrollbar,
bulk_staging: None, bulk_staging: None,
stash_entries: Default::default(),
_settings_subscription, _settings_subscription,
}; };
@ -2685,6 +2689,8 @@ impl GitPanel {
let repo = repo.read(cx); let repo = repo.read(cx);
self.stash_entries = repo.cached_stash();
for entry in repo.cached_status() { for entry in repo.cached_status() {
let is_conflict = repo.had_conflict_on_last_merge_head_change(&entry.repo_path); let is_conflict = repo.had_conflict_on_last_merge_head_change(&entry.repo_path);
let is_new = entry.status.is_created(); let is_new = entry.status.is_created();
@ -3053,6 +3059,7 @@ impl GitPanel {
let has_staged_changes = self.has_staged_changes(); let has_staged_changes = self.has_staged_changes();
let has_unstaged_changes = self.has_unstaged_changes(); let has_unstaged_changes = self.has_unstaged_changes();
let has_new_changes = self.new_count > 0; let has_new_changes = self.new_count > 0;
let has_stash_items = self.stash_entries.entries.len() > 0;
PopoverMenu::new(id.into()) PopoverMenu::new(id.into())
.trigger( .trigger(
@ -3068,6 +3075,7 @@ impl GitPanel {
has_staged_changes, has_staged_changes,
has_unstaged_changes, has_unstaged_changes,
has_new_changes, has_new_changes,
has_stash_items,
}, },
window, window,
cx, cx,
@ -4115,6 +4123,7 @@ impl GitPanel {
has_staged_changes: self.has_staged_changes(), has_staged_changes: self.has_staged_changes(),
has_unstaged_changes: self.has_unstaged_changes(), has_unstaged_changes: self.has_unstaged_changes(),
has_new_changes: self.new_count > 0, has_new_changes: self.new_count > 0,
has_stash_items: self.stash_entries.entries.len() > 0,
}, },
window, window,
cx, cx,

View file

@ -28,6 +28,7 @@ use git::{
GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath,
ResetMode, UpstreamTrackingStatus, ResetMode, UpstreamTrackingStatus,
}, },
stash::GitStash,
status::{ status::{
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode, FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
}, },
@ -248,6 +249,7 @@ pub struct RepositorySnapshot {
pub merge: MergeDetails, pub merge: MergeDetails,
pub remote_origin_url: Option<String>, pub remote_origin_url: Option<String>,
pub remote_upstream_url: Option<String>, pub remote_upstream_url: Option<String>,
pub stash_entries: GitStash,
} }
type JobId = u64; type JobId = u64;
@ -2744,6 +2746,7 @@ impl RepositorySnapshot {
merge: Default::default(), merge: Default::default(),
remote_origin_url: None, remote_origin_url: None,
remote_upstream_url: None, remote_upstream_url: None,
stash_entries: Default::default(),
} }
} }
@ -3264,6 +3267,10 @@ impl Repository {
self.snapshot.status() self.snapshot.status()
} }
pub fn cached_stash(&self) -> GitStash {
self.snapshot.stash_entries.clone()
}
pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> { pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> {
let git_store = self.git_store.upgrade()?; let git_store = self.git_store.upgrade()?;
let worktree_store = git_store.read(cx).worktree_store.read(cx); let worktree_store = git_store.read(cx).worktree_store.read(cx);
@ -4539,6 +4546,7 @@ impl Repository {
updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>, updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
println!("paths changed with updates_tx: {:?}", updates_tx);
self.paths_needing_status_update.extend(paths); self.paths_needing_status_update.extend(paths);
let this = cx.weak_entity(); let this = cx.weak_entity();
@ -4561,6 +4569,7 @@ impl Repository {
return Ok(()); return Ok(());
} }
let statuses = backend.status(&paths).await?; let statuses = backend.status(&paths).await?;
let stash_entries = backend.stash_entries().await?;
let changed_path_statuses = cx let changed_path_statuses = cx
.background_spawn(async move { .background_spawn(async move {
@ -4592,6 +4601,7 @@ impl Repository {
.await; .await;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.snapshot.stash_entries = stash_entries;
if !changed_path_statuses.is_empty() { if !changed_path_statuses.is_empty() {
this.snapshot this.snapshot
.statuses_by_path .statuses_by_path
@ -4852,6 +4862,7 @@ async fn compute_snapshot(
let statuses = backend let statuses = backend
.status(std::slice::from_ref(&WORK_DIRECTORY_REPO_PATH)) .status(std::slice::from_ref(&WORK_DIRECTORY_REPO_PATH))
.await?; .await?;
let stash_entries = backend.stash_entries().await?;
let statuses_by_path = SumTree::from_iter( let statuses_by_path = SumTree::from_iter(
statuses statuses
.entries .entries
@ -4902,6 +4913,7 @@ async fn compute_snapshot(
merge: merge_details, merge: merge_details,
remote_origin_url, remote_origin_url,
remote_upstream_url, remote_upstream_url,
stash_entries,
}; };
Ok((snapshot, events)) Ok((snapshot, events))