Finish removing git repository state and scanning logic from worktrees (#27568)
This PR completes the process of moving git repository state storage and scanning logic from the worktree crate to `project::git_store`. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
parent
8f25251faf
commit
e7290df02b
39 changed files with 3121 additions and 3529 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -10591,6 +10591,7 @@ dependencies = [
|
|||
"fuzzy",
|
||||
"git",
|
||||
"git2",
|
||||
"git_hosting_providers",
|
||||
"globset",
|
||||
"gpui",
|
||||
"http_client",
|
||||
|
@ -17217,7 +17218,6 @@ dependencies = [
|
|||
"fuzzy",
|
||||
"git",
|
||||
"git2",
|
||||
"git_hosting_providers",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"ignore",
|
||||
|
|
|
@ -10,10 +10,10 @@ use gpui::{
|
|||
use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
|
||||
use project::{
|
||||
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
|
||||
ProjectEnvironmentEvent, WorktreeId,
|
||||
ProjectEnvironmentEvent,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use std::{cmp::Reverse, fmt::Write, path::Path, sync::Arc, time::Duration};
|
||||
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
use workspace::{StatusItemView, Workspace, item::ItemHandle};
|
||||
|
@ -218,13 +218,14 @@ impl ActivityIndicator {
|
|||
fn pending_environment_errors<'a>(
|
||||
&'a self,
|
||||
cx: &'a App,
|
||||
) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
|
||||
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
|
||||
self.project.read(cx).shell_environment_errors(cx)
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
|
||||
// Show if any direnv calls failed
|
||||
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
|
||||
if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
|
||||
let abs_path = abs_path.clone();
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Warning)
|
||||
|
@ -234,7 +235,7 @@ impl ActivityIndicator {
|
|||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.remove_environment_error(worktree_id, cx);
|
||||
project.remove_environment_error(&abs_path, cx);
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
|
|
|
@ -11,7 +11,7 @@ use collections::{BTreeMap, HashMap, HashSet};
|
|||
use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt as _};
|
||||
use git;
|
||||
use git::repository::DiffType;
|
||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
|
@ -19,7 +19,7 @@ use language_model::{
|
|||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason, TokenUsage,
|
||||
};
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint};
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
|
||||
use project::{Project, Worktree};
|
||||
use prompt_store::{
|
||||
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
|
||||
|
@ -1446,48 +1446,61 @@ impl Thread {
|
|||
(path, snapshot)
|
||||
});
|
||||
|
||||
let Ok((worktree_path, snapshot)) = worktree_info else {
|
||||
let Ok((worktree_path, _snapshot)) = worktree_info else {
|
||||
return WorktreeSnapshot {
|
||||
worktree_path: String::new(),
|
||||
git_state: None,
|
||||
};
|
||||
};
|
||||
|
||||
let repo_info = git_store
|
||||
let git_state = git_store
|
||||
.update(cx, |git_store, cx| {
|
||||
git_store
|
||||
.repositories()
|
||||
.values()
|
||||
.find(|repo| repo.read(cx).worktree_id == Some(snapshot.id()))
|
||||
.and_then(|repo| {
|
||||
let repo = repo.read(cx);
|
||||
Some((repo.branch().cloned(), repo.local_repository()?))
|
||||
.find(|repo| {
|
||||
repo.read(cx)
|
||||
.abs_path_to_repo_path(&worktree.read(cx).abs_path())
|
||||
.is_some()
|
||||
})
|
||||
.cloned()
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
.flatten()
|
||||
.map(|repo| {
|
||||
repo.read_with(cx, |repo, _| {
|
||||
let current_branch =
|
||||
repo.branch.as_ref().map(|branch| branch.name.to_string());
|
||||
repo.send_job(|state, _| async move {
|
||||
let RepositoryState::Local { backend, .. } = state else {
|
||||
return GitState {
|
||||
remote_url: None,
|
||||
head_sha: None,
|
||||
current_branch,
|
||||
diff: None,
|
||||
};
|
||||
};
|
||||
|
||||
// Extract git information
|
||||
let git_state = match repo_info {
|
||||
None => None,
|
||||
Some((branch, repo)) => {
|
||||
let current_branch = branch.map(|branch| branch.name.to_string());
|
||||
let remote_url = repo.remote_url("origin");
|
||||
let head_sha = repo.head_sha();
|
||||
let remote_url = backend.remote_url("origin");
|
||||
let head_sha = backend.head_sha();
|
||||
let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
|
||||
|
||||
// Get diff asynchronously
|
||||
let diff = repo
|
||||
.diff(git::repository::DiffType::HeadToWorktree)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Some(GitState {
|
||||
remote_url,
|
||||
head_sha,
|
||||
current_branch,
|
||||
diff,
|
||||
GitState {
|
||||
remote_url,
|
||||
head_sha,
|
||||
current_branch,
|
||||
diff,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let git_state = match git_state {
|
||||
Some(git_state) => match git_state.ok() {
|
||||
Some(git_state) => git_state.await.ok(),
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
WorktreeSnapshot {
|
||||
|
|
|
@ -469,7 +469,7 @@ impl Room {
|
|||
let repository = repository.read(cx);
|
||||
repositories.push(proto::RejoinRepository {
|
||||
id: entry_id.to_proto(),
|
||||
scan_id: repository.completed_scan_id as u64,
|
||||
scan_id: repository.scan_id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -334,7 +334,7 @@ impl Database {
|
|||
project_repository::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
legacy_worktree_id: ActiveValue::set(Some(worktree_id)),
|
||||
id: ActiveValue::set(repository.work_directory_id as i64),
|
||||
id: ActiveValue::set(repository.repository_id as i64),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
branch_summary: ActiveValue::Set(
|
||||
|
@ -384,7 +384,7 @@ impl Database {
|
|||
project_repository_statuses::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
repository_id: ActiveValue::set(
|
||||
repository.work_directory_id as i64,
|
||||
repository.repository_id as i64,
|
||||
),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
|
@ -424,7 +424,7 @@ impl Database {
|
|||
.eq(project_id)
|
||||
.and(
|
||||
project_repository_statuses::Column::RepositoryId
|
||||
.eq(repo.work_directory_id),
|
||||
.eq(repo.repository_id),
|
||||
)
|
||||
.and(
|
||||
project_repository_statuses::Column::RepoPath
|
||||
|
@ -936,7 +936,7 @@ impl Database {
|
|||
worktree.legacy_repository_entries.insert(
|
||||
db_repository_entry.id as u64,
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.id as u64,
|
||||
repository_id: db_repository_entry.id as u64,
|
||||
updated_statuses,
|
||||
removed_statuses: Vec::new(),
|
||||
current_merge_conflicts,
|
||||
|
@ -955,6 +955,7 @@ impl Database {
|
|||
current_merge_conflicts,
|
||||
branch_summary,
|
||||
scan_id: db_repository_entry.scan_id as u64,
|
||||
is_last_update: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -764,7 +764,7 @@ impl Database {
|
|||
.find(|worktree| worktree.id as i64 == legacy_worktree_id)
|
||||
{
|
||||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||
work_directory_id: db_repository.id as u64,
|
||||
repository_id: db_repository.id as u64,
|
||||
updated_statuses,
|
||||
removed_statuses,
|
||||
current_merge_conflicts,
|
||||
|
@ -782,6 +782,7 @@ impl Database {
|
|||
id: db_repository.id as u64,
|
||||
abs_path: db_repository.abs_path,
|
||||
scan_id: db_repository.scan_id as u64,
|
||||
is_last_update: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2898,8 +2898,8 @@ async fn test_git_branch_name(
|
|||
assert_eq!(
|
||||
repository
|
||||
.read(cx)
|
||||
.repository_entry
|
||||
.branch()
|
||||
.branch
|
||||
.as_ref()
|
||||
.map(|branch| branch.name.to_string()),
|
||||
branch_name
|
||||
)
|
||||
|
@ -3033,7 +3033,6 @@ async fn test_git_status_sync(
|
|||
let repo = repos.into_iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.read(cx)
|
||||
.repository_entry
|
||||
.status_for_path(&file.into())
|
||||
.map(|entry| entry.status),
|
||||
status
|
||||
|
@ -6882,7 +6881,8 @@ async fn test_remote_git_branches(
|
|||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.current_branch()
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone()
|
||||
})
|
||||
|
@ -6919,7 +6919,8 @@ async fn test_remote_git_branches(
|
|||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.current_branch()
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone()
|
||||
})
|
||||
|
|
|
@ -1181,6 +1181,10 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
(worktree.id(), worktree.snapshot())
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let host_repository_snapshots = host_project.read_with(host_cx, |host_project, cx| {
|
||||
host_project.git_store().read(cx).repo_snapshots(cx)
|
||||
});
|
||||
let guest_repository_snapshots = guest_project.git_store().read(cx).repo_snapshots(cx);
|
||||
|
||||
assert_eq!(
|
||||
guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
|
||||
|
@ -1189,6 +1193,13 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
client.username, guest_project.remote_id(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
guest_repository_snapshots.values().collect::<Vec<_>>(),
|
||||
host_repository_snapshots.values().collect::<Vec<_>>(),
|
||||
"{} has different repositories than the host for project {:?}",
|
||||
client.username, guest_project.remote_id(),
|
||||
);
|
||||
|
||||
for (id, host_snapshot) in &host_worktree_snapshots {
|
||||
let guest_snapshot = &guest_worktree_snapshots[id];
|
||||
assert_eq!(
|
||||
|
@ -1216,12 +1227,6 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
id,
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.repositories().iter().collect::<Vec<_>>(), host_snapshot.repositories().iter().collect::<Vec<_>>(),
|
||||
"{} has different repositories than the host for worktree {:?} and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
|
||||
"{} has different scan id than the host for worktree {:?} and project {:?}",
|
||||
client.username,
|
||||
|
|
|
@ -313,7 +313,8 @@ async fn test_ssh_collaboration_git_branches(
|
|||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.current_branch()
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone()
|
||||
})
|
||||
|
@ -352,7 +353,8 @@ async fn test_ssh_collaboration_git_branches(
|
|||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.current_branch()
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone()
|
||||
})
|
||||
|
|
|
@ -12,7 +12,10 @@ use gpui::{
|
|||
};
|
||||
use language::{Bias, Buffer, BufferSnapshot, Edit};
|
||||
use multi_buffer::RowInfo;
|
||||
use project::{Project, ProjectItem, git_store::Repository};
|
||||
use project::{
|
||||
Project, ProjectItem,
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent},
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use sum_tree::SumTree;
|
||||
|
@ -202,13 +205,21 @@ impl GitBlame {
|
|||
this.generate(cx);
|
||||
}
|
||||
}
|
||||
project::Event::GitStateUpdated => {
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
let git_store_subscription =
|
||||
cx.subscribe(&git_store, move |this, _, event, cx| match event {
|
||||
GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated, _)
|
||||
| GitStoreEvent::RepositoryAdded(_)
|
||||
| GitStoreEvent::RepositoryRemoved(_) => {
|
||||
log::debug!("Status of git repositories updated. Regenerating blame data...",);
|
||||
this.generate(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
|
@ -226,7 +237,11 @@ impl GitBlame {
|
|||
task: Task::ready(Ok(())),
|
||||
generated: false,
|
||||
regenerate_on_edit_task: Task::ready(Ok(())),
|
||||
_regenerate_subscriptions: vec![buffer_subscriptions, project_subscription],
|
||||
_regenerate_subscriptions: vec![
|
||||
buffer_subscriptions,
|
||||
project_subscription,
|
||||
git_store_subscription,
|
||||
],
|
||||
};
|
||||
this.generate(cx);
|
||||
this
|
||||
|
|
|
@ -123,7 +123,7 @@ impl GitRepository for FakeGitRepository {
|
|||
&self,
|
||||
path: RepoPath,
|
||||
content: Option<String>,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<anyhow::Result<()>> {
|
||||
self.with_state_async(true, move |state| {
|
||||
if let Some(message) = state.simulated_index_write_error_message.clone() {
|
||||
|
@ -157,7 +157,7 @@ impl GitRepository for FakeGitRepository {
|
|||
&self,
|
||||
_commit: String,
|
||||
_mode: ResetMode,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ impl GitRepository for FakeGitRepository {
|
|||
&self,
|
||||
_commit: String,
|
||||
_paths: Vec<RepoPath>,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
@ -179,7 +179,11 @@ impl GitRepository for FakeGitRepository {
|
|||
self.path()
|
||||
}
|
||||
|
||||
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||
fn merge_message(&self) -> BoxFuture<Option<String>> {
|
||||
async move { None }.boxed()
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
|
||||
let workdir_path = self.dot_git_path.parent().unwrap();
|
||||
|
||||
// Load gitignores
|
||||
|
@ -221,7 +225,7 @@ impl GitRepository for FakeGitRepository {
|
|||
})
|
||||
.collect();
|
||||
|
||||
self.fs.with_git_state(&self.dot_git_path, false, |state| {
|
||||
let result = self.fs.with_git_state(&self.dot_git_path, false, |state| {
|
||||
let mut entries = Vec::new();
|
||||
let paths = state
|
||||
.head_contents
|
||||
|
@ -302,10 +306,11 @@ impl GitRepository for FakeGitRepository {
|
|||
}
|
||||
}
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok(GitStatus {
|
||||
anyhow::Ok(GitStatus {
|
||||
entries: entries.into(),
|
||||
})
|
||||
})?
|
||||
});
|
||||
async move { result? }.boxed()
|
||||
}
|
||||
|
||||
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
|
||||
|
@ -351,7 +356,7 @@ impl GitRepository for FakeGitRepository {
|
|||
fn stage_paths(
|
||||
&self,
|
||||
_paths: Vec<RepoPath>,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
@ -359,7 +364,7 @@ impl GitRepository for FakeGitRepository {
|
|||
fn unstage_paths(
|
||||
&self,
|
||||
_paths: Vec<RepoPath>,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
@ -368,7 +373,7 @@ impl GitRepository for FakeGitRepository {
|
|||
&self,
|
||||
_message: gpui::SharedString,
|
||||
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
@ -379,7 +384,7 @@ impl GitRepository for FakeGitRepository {
|
|||
_remote: String,
|
||||
_options: Option<PushOptions>,
|
||||
_askpass: AskPassDelegate,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
|
||||
unimplemented!()
|
||||
|
@ -390,7 +395,7 @@ impl GitRepository for FakeGitRepository {
|
|||
_branch: String,
|
||||
_remote: String,
|
||||
_askpass: AskPassDelegate,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
|
||||
unimplemented!()
|
||||
|
@ -399,7 +404,7 @@ impl GitRepository for FakeGitRepository {
|
|||
fn fetch(
|
||||
&self,
|
||||
_askpass: AskPassDelegate,
|
||||
_env: HashMap<String, String>,
|
||||
_env: Arc<HashMap<String, String>>,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
|
||||
unimplemented!()
|
||||
|
|
|
@ -188,7 +188,7 @@ pub trait GitRepository: Send + Sync {
|
|||
&self,
|
||||
path: RepoPath,
|
||||
content: Option<String>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<anyhow::Result<()>>;
|
||||
|
||||
/// Returns the URL of the remote with the given name.
|
||||
|
@ -199,7 +199,9 @@ pub trait GitRepository: Send + Sync {
|
|||
|
||||
fn merge_head_shas(&self) -> Vec<String>;
|
||||
|
||||
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
|
||||
fn merge_message(&self) -> BoxFuture<Option<String>>;
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>>;
|
||||
|
||||
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
|
||||
|
||||
|
@ -210,14 +212,14 @@ pub trait GitRepository: Send + Sync {
|
|||
&self,
|
||||
commit: String,
|
||||
mode: ResetMode,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>>;
|
||||
|
||||
fn checkout_files(
|
||||
&self,
|
||||
commit: String,
|
||||
paths: Vec<RepoPath>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>>;
|
||||
|
||||
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
|
||||
|
@ -243,7 +245,7 @@ pub trait GitRepository: Send + Sync {
|
|||
fn stage_paths(
|
||||
&self,
|
||||
paths: Vec<RepoPath>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>>;
|
||||
/// Updates the index to match HEAD at the given paths.
|
||||
///
|
||||
|
@ -251,14 +253,14 @@ pub trait GitRepository: Send + Sync {
|
|||
fn unstage_paths(
|
||||
&self,
|
||||
paths: Vec<RepoPath>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>>;
|
||||
|
||||
fn commit(
|
||||
&self,
|
||||
message: SharedString,
|
||||
name_and_email: Option<(SharedString, SharedString)>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>>;
|
||||
|
||||
fn push(
|
||||
|
@ -267,7 +269,7 @@ pub trait GitRepository: Send + Sync {
|
|||
upstream_name: String,
|
||||
options: Option<PushOptions>,
|
||||
askpass: AskPassDelegate,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
// This method takes an AsyncApp to ensure it's invoked on the main thread,
|
||||
// otherwise git-credentials-manager won't work.
|
||||
cx: AsyncApp,
|
||||
|
@ -278,7 +280,7 @@ pub trait GitRepository: Send + Sync {
|
|||
branch_name: String,
|
||||
upstream_name: String,
|
||||
askpass: AskPassDelegate,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
// This method takes an AsyncApp to ensure it's invoked on the main thread,
|
||||
// otherwise git-credentials-manager won't work.
|
||||
cx: AsyncApp,
|
||||
|
@ -287,7 +289,7 @@ pub trait GitRepository: Send + Sync {
|
|||
fn fetch(
|
||||
&self,
|
||||
askpass: AskPassDelegate,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
// This method takes an AsyncApp to ensure it's invoked on the main thread,
|
||||
// otherwise git-credentials-manager won't work.
|
||||
cx: AsyncApp,
|
||||
|
@ -528,7 +530,7 @@ impl GitRepository for RealGitRepository {
|
|||
&self,
|
||||
commit: String,
|
||||
mode: ResetMode,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
async move {
|
||||
let working_directory = self.working_directory();
|
||||
|
@ -539,7 +541,7 @@ impl GitRepository for RealGitRepository {
|
|||
};
|
||||
|
||||
let output = new_smol_command(&self.git_binary_path)
|
||||
.envs(env)
|
||||
.envs(env.iter())
|
||||
.current_dir(&working_directory?)
|
||||
.args(["reset", mode_flag, &commit])
|
||||
.output()
|
||||
|
@ -559,7 +561,7 @@ impl GitRepository for RealGitRepository {
|
|||
&self,
|
||||
commit: String,
|
||||
paths: Vec<RepoPath>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
@ -570,7 +572,7 @@ impl GitRepository for RealGitRepository {
|
|||
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory?)
|
||||
.envs(env)
|
||||
.envs(env.iter())
|
||||
.args(["checkout", &commit, "--"])
|
||||
.args(paths.iter().map(|path| path.as_ref()))
|
||||
.output()
|
||||
|
@ -640,7 +642,7 @@ impl GitRepository for RealGitRepository {
|
|||
&self,
|
||||
path: RepoPath,
|
||||
content: Option<String>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<anyhow::Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
@ -650,7 +652,7 @@ impl GitRepository for RealGitRepository {
|
|||
if let Some(content) = content {
|
||||
let mut child = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(&env)
|
||||
.envs(env.iter())
|
||||
.args(["hash-object", "-w", "--stdin"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
@ -668,7 +670,7 @@ impl GitRepository for RealGitRepository {
|
|||
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.envs(env.iter())
|
||||
.args(["update-index", "--add", "--cacheinfo", "100644", &sha])
|
||||
.arg(path.to_unix_style())
|
||||
.output()
|
||||
|
@ -683,7 +685,7 @@ impl GitRepository for RealGitRepository {
|
|||
} else {
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.envs(env)
|
||||
.envs(env.iter())
|
||||
.args(["update-index", "--force-remove"])
|
||||
.arg(path.to_unix_style())
|
||||
.output()
|
||||
|
@ -733,18 +735,30 @@ impl GitRepository for RealGitRepository {
|
|||
shas
|
||||
}
|
||||
|
||||
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||
let output = new_std_command(&self.git_binary_path)
|
||||
.current_dir(self.working_directory()?)
|
||||
.args(git_status_args(path_prefixes))
|
||||
.output()?;
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
stdout.parse()
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(anyhow!("git status failed: {}", stderr))
|
||||
}
|
||||
fn merge_message(&self) -> BoxFuture<Option<String>> {
|
||||
let path = self.path().join("MERGE_MSG");
|
||||
async move { std::fs::read_to_string(&path).ok() }.boxed()
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
let working_directory = self.working_directory();
|
||||
let path_prefixes = path_prefixes.to_owned();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let output = new_std_command(&git_binary_path)
|
||||
.current_dir(working_directory?)
|
||||
.args(git_status_args(&path_prefixes))
|
||||
.output()?;
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
stdout.parse()
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(anyhow!("git status failed: {}", stderr))
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
|
||||
|
@ -891,7 +905,7 @@ impl GitRepository for RealGitRepository {
|
|||
fn stage_paths(
|
||||
&self,
|
||||
paths: Vec<RepoPath>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
@ -900,7 +914,7 @@ impl GitRepository for RealGitRepository {
|
|||
if !paths.is_empty() {
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory?)
|
||||
.envs(env)
|
||||
.envs(env.iter())
|
||||
.args(["update-index", "--add", "--remove", "--"])
|
||||
.args(paths.iter().map(|p| p.to_unix_style()))
|
||||
.output()
|
||||
|
@ -921,7 +935,7 @@ impl GitRepository for RealGitRepository {
|
|||
fn unstage_paths(
|
||||
&self,
|
||||
paths: Vec<RepoPath>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.git_binary_path.clone();
|
||||
|
@ -931,7 +945,7 @@ impl GitRepository for RealGitRepository {
|
|||
if !paths.is_empty() {
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory?)
|
||||
.envs(env)
|
||||
.envs(env.iter())
|
||||
.args(["reset", "--quiet", "--"])
|
||||
.args(paths.iter().map(|p| p.as_ref()))
|
||||
.output()
|
||||
|
@ -953,14 +967,14 @@ impl GitRepository for RealGitRepository {
|
|||
&self,
|
||||
message: SharedString,
|
||||
name_and_email: Option<(SharedString, SharedString)>,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
let working_directory = self.working_directory();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let mut cmd = new_smol_command("git");
|
||||
cmd.current_dir(&working_directory?)
|
||||
.envs(env)
|
||||
.envs(env.iter())
|
||||
.args(["commit", "--quiet", "-m"])
|
||||
.arg(&message.to_string())
|
||||
.arg("--cleanup=strip");
|
||||
|
@ -988,7 +1002,7 @@ impl GitRepository for RealGitRepository {
|
|||
remote_name: String,
|
||||
options: Option<PushOptions>,
|
||||
ask_pass: AskPassDelegate,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<RemoteCommandOutput>> {
|
||||
let working_directory = self.working_directory();
|
||||
|
@ -997,7 +1011,7 @@ impl GitRepository for RealGitRepository {
|
|||
let working_directory = working_directory?;
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(&env)
|
||||
.envs(env.iter())
|
||||
.env("GIT_HTTP_USER_AGENT", "Zed")
|
||||
.current_dir(&working_directory)
|
||||
.args(["push"])
|
||||
|
@ -1021,7 +1035,7 @@ impl GitRepository for RealGitRepository {
|
|||
branch_name: String,
|
||||
remote_name: String,
|
||||
ask_pass: AskPassDelegate,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<RemoteCommandOutput>> {
|
||||
let working_directory = self.working_directory();
|
||||
|
@ -1029,7 +1043,7 @@ impl GitRepository for RealGitRepository {
|
|||
async move {
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(&env)
|
||||
.envs(env.iter())
|
||||
.env("GIT_HTTP_USER_AGENT", "Zed")
|
||||
.current_dir(&working_directory?)
|
||||
.args(["pull"])
|
||||
|
@ -1046,7 +1060,7 @@ impl GitRepository for RealGitRepository {
|
|||
fn fetch(
|
||||
&self,
|
||||
ask_pass: AskPassDelegate,
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
cx: AsyncApp,
|
||||
) -> BoxFuture<Result<RemoteCommandOutput>> {
|
||||
let working_directory = self.working_directory();
|
||||
|
@ -1054,7 +1068,7 @@ impl GitRepository for RealGitRepository {
|
|||
async move {
|
||||
let mut command = new_smol_command("git");
|
||||
command
|
||||
.envs(&env)
|
||||
.envs(env.iter())
|
||||
.env("GIT_HTTP_USER_AGENT", "Zed")
|
||||
.current_dir(&working_directory?)
|
||||
.args(["fetch", "--all"])
|
||||
|
@ -1467,7 +1481,7 @@ struct GitBinaryCommandError {
|
|||
}
|
||||
|
||||
async fn run_git_command(
|
||||
env: HashMap<String, String>,
|
||||
env: Arc<HashMap<String, String>>,
|
||||
ask_pass: AskPassDelegate,
|
||||
mut command: smol::process::Command,
|
||||
executor: &BackgroundExecutor,
|
||||
|
@ -1769,12 +1783,19 @@ mod tests {
|
|||
|
||||
let repo =
|
||||
RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
|
||||
repo.stage_paths(vec![RepoPath::from_str("file")], HashMap::default())
|
||||
.await
|
||||
.unwrap();
|
||||
repo.commit("Initial commit".into(), None, checkpoint_author_envs())
|
||||
.await
|
||||
.unwrap();
|
||||
repo.stage_paths(
|
||||
vec![RepoPath::from_str("file")],
|
||||
Arc::new(HashMap::default()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
repo.commit(
|
||||
"Initial commit".into(),
|
||||
None,
|
||||
Arc::new(checkpoint_author_envs()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
smol::fs::write(&file_path, "modified before checkpoint")
|
||||
.await
|
||||
|
@ -1791,13 +1812,16 @@ mod tests {
|
|||
smol::fs::write(&file_path, "modified after checkpoint")
|
||||
.await
|
||||
.unwrap();
|
||||
repo.stage_paths(vec![RepoPath::from_str("file")], HashMap::default())
|
||||
.await
|
||||
.unwrap();
|
||||
repo.stage_paths(
|
||||
vec![RepoPath::from_str("file")],
|
||||
Arc::new(HashMap::default()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
repo.commit(
|
||||
"Commit after checkpoint".into(),
|
||||
None,
|
||||
checkpoint_author_envs(),
|
||||
Arc::new(checkpoint_author_envs()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -1889,12 +1913,19 @@ mod tests {
|
|||
|
||||
let repo =
|
||||
RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
|
||||
repo.stage_paths(vec![RepoPath::from_str("file")], HashMap::default())
|
||||
.await
|
||||
.unwrap();
|
||||
repo.commit("Initial commit".into(), None, checkpoint_author_envs())
|
||||
.await
|
||||
.unwrap();
|
||||
repo.stage_paths(
|
||||
vec![RepoPath::from_str("file")],
|
||||
Arc::new(HashMap::default()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
repo.commit(
|
||||
"Initial commit".into(),
|
||||
None,
|
||||
Arc::new(checkpoint_author_envs()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let initial_commit_sha = repo.head_sha().unwrap();
|
||||
|
||||
|
@ -1912,13 +1943,17 @@ mod tests {
|
|||
RepoPath::from_str("new_file1"),
|
||||
RepoPath::from_str("new_file2"),
|
||||
],
|
||||
HashMap::default(),
|
||||
Arc::new(HashMap::default()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
repo.commit(
|
||||
"Commit new files".into(),
|
||||
None,
|
||||
Arc::new(checkpoint_author_envs()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
repo.commit("Commit new files".into(), None, checkpoint_author_envs())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
repo.restore_checkpoint(checkpoint).await.unwrap();
|
||||
assert_eq!(repo.head_sha().unwrap(), initial_commit_sha);
|
||||
|
@ -1935,7 +1970,7 @@ mod tests {
|
|||
"content2"
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_blocking(&[]).unwrap().entries.as_ref(),
|
||||
repo.status(&[]).await.unwrap().entries.as_ref(),
|
||||
&[
|
||||
(RepoPath::from_str("new_file1"), FileStatus::Untracked),
|
||||
(RepoPath::from_str("new_file2"), FileStatus::Untracked)
|
||||
|
|
|
@ -336,7 +336,7 @@ impl PickerDelegate for BranchListDelegate {
|
|||
|
||||
let current_branch = self.repo.as_ref().map(|repo| {
|
||||
repo.update(cx, |repo, _| {
|
||||
repo.current_branch().map(|branch| branch.name.clone())
|
||||
repo.branch.as_ref().map(|branch| branch.name.clone())
|
||||
})
|
||||
});
|
||||
|
||||
|
@ -463,7 +463,7 @@ impl PickerDelegate for BranchListDelegate {
|
|||
let message = if entry.is_new {
|
||||
if let Some(current_branch) =
|
||||
self.repo.as_ref().and_then(|repo| {
|
||||
repo.read(cx).current_branch().map(|b| b.name.clone())
|
||||
repo.read(cx).branch.as_ref().map(|b| b.name.clone())
|
||||
})
|
||||
{
|
||||
format!("based off {}", current_branch)
|
||||
|
|
|
@ -234,7 +234,7 @@ impl CommitModal {
|
|||
|
||||
let branch = active_repo
|
||||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).repository_entry.branch())
|
||||
.and_then(|repo| repo.read(cx).branch.as_ref())
|
||||
.map(|b| b.name.clone())
|
||||
.unwrap_or_else(|| "<no branch>".into());
|
||||
|
||||
|
|
|
@ -45,9 +45,10 @@ use panel::{
|
|||
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||
panel_icon_button,
|
||||
};
|
||||
use project::git_store::RepositoryEvent;
|
||||
use project::{
|
||||
Fs, Project, ProjectPath,
|
||||
git_store::{GitEvent, Repository},
|
||||
git_store::{GitStoreEvent, Repository},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
|
@ -340,7 +341,7 @@ const MAX_PANEL_EDITOR_LINES: usize = 6;
|
|||
|
||||
pub(crate) fn commit_message_editor(
|
||||
commit_message_buffer: Entity<Buffer>,
|
||||
placeholder: Option<&str>,
|
||||
placeholder: Option<SharedString>,
|
||||
project: Entity<Project>,
|
||||
in_panel: bool,
|
||||
window: &mut Window,
|
||||
|
@ -361,7 +362,7 @@ pub(crate) fn commit_message_editor(
|
|||
commit_editor.set_show_wrap_guides(false, cx);
|
||||
commit_editor.set_show_indent_guides(false, cx);
|
||||
commit_editor.set_hard_wrap(Some(72), cx);
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message");
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message".into());
|
||||
commit_editor.set_placeholder_text(placeholder, cx);
|
||||
commit_editor
|
||||
}
|
||||
|
@ -403,14 +404,18 @@ impl GitPanel {
|
|||
&git_store,
|
||||
window,
|
||||
move |this, git_store, event, window, cx| match event {
|
||||
GitEvent::FileSystemUpdated => {
|
||||
this.schedule_update(false, window, cx);
|
||||
}
|
||||
GitEvent::ActiveRepositoryChanged | GitEvent::GitStateUpdated => {
|
||||
GitStoreEvent::ActiveRepositoryChanged(_) => {
|
||||
this.active_repository = git_store.read(cx).active_repository();
|
||||
this.schedule_update(true, window, cx);
|
||||
}
|
||||
GitEvent::IndexWriteError(error) => {
|
||||
GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated, true) => {
|
||||
this.schedule_update(true, window, cx);
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
|
||||
GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
|
||||
this.schedule_update(false, window, cx);
|
||||
}
|
||||
GitStoreEvent::IndexWriteError(error) => {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_error(error, cx);
|
||||
|
@ -828,7 +833,7 @@ impl GitPanel {
|
|||
.active_repository
|
||||
.as_ref()
|
||||
.map_or(false, |active_repository| {
|
||||
active_repository.read(cx).entry_count() > 0
|
||||
active_repository.read(cx).status_summary().count > 0
|
||||
});
|
||||
if have_entries && self.selected_entry.is_none() {
|
||||
self.selected_entry = Some(1);
|
||||
|
@ -1415,7 +1420,7 @@ impl GitPanel {
|
|||
let message = self.commit_editor.read(cx).text(cx);
|
||||
|
||||
if !message.trim().is_empty() {
|
||||
return Some(message.to_string());
|
||||
return Some(message);
|
||||
}
|
||||
|
||||
self.suggest_commit_message(cx)
|
||||
|
@ -1593,7 +1598,7 @@ impl GitPanel {
|
|||
.as_ref()
|
||||
.and_then(|repo| repo.read(cx).merge_message.as_ref())
|
||||
{
|
||||
return Some(merge_message.clone());
|
||||
return Some(merge_message.to_string());
|
||||
}
|
||||
|
||||
let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
|
||||
|
@ -1849,7 +1854,7 @@ impl GitPanel {
|
|||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(branch) = repo.read(cx).current_branch() else {
|
||||
let Some(branch) = repo.read(cx).branch.as_ref() else {
|
||||
return;
|
||||
};
|
||||
telemetry::event!("Git Pulled");
|
||||
|
@ -1906,7 +1911,7 @@ impl GitPanel {
|
|||
let Some(repo) = self.active_repository.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(branch) = repo.read(cx).current_branch() else {
|
||||
let Some(branch) = repo.read(cx).branch.as_ref() else {
|
||||
return;
|
||||
};
|
||||
telemetry::event!("Git Pushed");
|
||||
|
@ -2019,7 +2024,7 @@ impl GitPanel {
|
|||
|
||||
let mut current_remotes: Vec<Remote> = repo
|
||||
.update(&mut cx, |repo, _| {
|
||||
let Some(current_branch) = repo.current_branch() else {
|
||||
let Some(current_branch) = repo.branch.as_ref() else {
|
||||
return Err(anyhow::anyhow!("No active branch"));
|
||||
};
|
||||
|
||||
|
@ -2215,7 +2220,7 @@ impl GitPanel {
|
|||
git_panel.commit_editor = cx.new(|cx| {
|
||||
commit_message_editor(
|
||||
buffer,
|
||||
git_panel.suggest_commit_message(cx).as_deref(),
|
||||
git_panel.suggest_commit_message(cx).map(SharedString::from),
|
||||
git_panel.project.clone(),
|
||||
true,
|
||||
window,
|
||||
|
@ -2275,10 +2280,7 @@ impl GitPanel {
|
|||
continue;
|
||||
}
|
||||
|
||||
let abs_path = repo
|
||||
.repository_entry
|
||||
.work_directory_abs_path
|
||||
.join(&entry.repo_path.0);
|
||||
let abs_path = repo.work_directory_abs_path.join(&entry.repo_path.0);
|
||||
let entry = GitStatusEntry {
|
||||
repo_path: entry.repo_path.clone(),
|
||||
abs_path,
|
||||
|
@ -2392,9 +2394,7 @@ impl GitPanel {
|
|||
self.select_first_entry_if_none(cx);
|
||||
|
||||
let suggested_commit_message = self.suggest_commit_message(cx);
|
||||
let placeholder_text = suggested_commit_message
|
||||
.as_deref()
|
||||
.unwrap_or("Enter commit message");
|
||||
let placeholder_text = suggested_commit_message.unwrap_or("Enter commit message".into());
|
||||
|
||||
self.commit_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
|
||||
|
@ -2823,12 +2823,7 @@ impl GitPanel {
|
|||
}
|
||||
|
||||
pub(crate) fn render_remote_button(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let branch = self
|
||||
.active_repository
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.current_branch()
|
||||
.cloned();
|
||||
let branch = self.active_repository.as_ref()?.read(cx).branch.clone();
|
||||
if !self.can_push_and_pull(cx) {
|
||||
return None;
|
||||
}
|
||||
|
@ -2868,7 +2863,7 @@ impl GitPanel {
|
|||
let commit_tooltip_focus_handle = editor_focus_handle.clone();
|
||||
let expand_tooltip_focus_handle = editor_focus_handle.clone();
|
||||
|
||||
let branch = active_repository.read(cx).current_branch().cloned();
|
||||
let branch = active_repository.read(cx).branch.clone();
|
||||
|
||||
let footer_size = px(32.);
|
||||
let gap = px(9.0);
|
||||
|
@ -2999,7 +2994,7 @@ 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).current_branch()?;
|
||||
let branch = active_repository.read(cx).branch.as_ref()?;
|
||||
let commit = branch.most_recent_commit.as_ref()?.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt};
|
|||
use multi_buffer::{MultiBuffer, PathKey};
|
||||
use project::{
|
||||
Project, ProjectPath,
|
||||
git_store::{GitEvent, GitStore},
|
||||
git_store::{GitStore, GitStoreEvent, RepositoryEvent},
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
use theme::ActiveTheme;
|
||||
|
@ -153,9 +153,8 @@ impl ProjectDiff {
|
|||
&git_store,
|
||||
window,
|
||||
move |this, _git_store, event, _window, _cx| match event {
|
||||
GitEvent::ActiveRepositoryChanged
|
||||
| GitEvent::FileSystemUpdated
|
||||
| GitEvent::GitStateUpdated => {
|
||||
GitStoreEvent::ActiveRepositoryChanged(_)
|
||||
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated, true) => {
|
||||
*this.update_needed.borrow_mut() = ();
|
||||
}
|
||||
_ => {}
|
||||
|
@ -452,13 +451,11 @@ impl ProjectDiff {
|
|||
) -> Result<()> {
|
||||
while let Some(_) = recv.next().await {
|
||||
this.update(cx, |this, cx| {
|
||||
let new_branch =
|
||||
this.git_store
|
||||
.read(cx)
|
||||
.active_repository()
|
||||
.and_then(|active_repository| {
|
||||
active_repository.read(cx).current_branch().cloned()
|
||||
});
|
||||
let new_branch = this
|
||||
.git_store
|
||||
.read(cx)
|
||||
.active_repository()
|
||||
.and_then(|active_repository| active_repository.read(cx).branch.clone());
|
||||
if new_branch != this.current_branch {
|
||||
this.current_branch = new_branch;
|
||||
cx.notify();
|
||||
|
@ -1499,6 +1496,7 @@ mod tests {
|
|||
.unindent(),
|
||||
);
|
||||
|
||||
eprintln!(">>>>>>>> git restore");
|
||||
let prev_buffer_hunks =
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
let snapshot = buffer_editor.snapshot(window, cx);
|
||||
|
@ -1516,14 +1514,13 @@ mod tests {
|
|||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
let snapshot = buffer_editor.snapshot(window, cx);
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let new_buffer_hunks = buffer_editor
|
||||
buffer_editor
|
||||
.diff_hunks_in_ranges(&[editor::Anchor::min()..editor::Anchor::max()], snapshot)
|
||||
.collect::<Vec<_>>();
|
||||
buffer_editor.git_restore(&Default::default(), window, cx);
|
||||
new_buffer_hunks
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(new_buffer_hunks.as_slice(), &[]);
|
||||
|
||||
eprintln!(">>>>>>>> modify");
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
buffer_editor.set_text("different\n", window, cx);
|
||||
buffer_editor.save(false, project.clone(), window, cx)
|
||||
|
@ -1533,6 +1530,20 @@ mod tests {
|
|||
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
|
||||
buffer_editor.expand_all_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
|
||||
assert_state_with_diff(
|
||||
&buffer_editor,
|
||||
cx,
|
||||
&"
|
||||
- original
|
||||
+ different
|
||||
ˇ"
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
assert_state_with_diff(
|
||||
&diff_editor,
|
||||
cx,
|
||||
|
|
|
@ -2475,6 +2475,7 @@ impl MultiBuffer {
|
|||
let buffer_id = diff.buffer_id;
|
||||
let buffers = self.buffers.borrow();
|
||||
let Some(buffer_state) = buffers.get(&buffer_id) else {
|
||||
eprintln!("no buffer");
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ fs.workspace = true
|
|||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
git_hosting_providers.workspace = true
|
||||
globset.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
|
|
|
@ -872,21 +872,6 @@ impl BufferStore {
|
|||
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
|
||||
}
|
||||
|
||||
pub(crate) fn worktree_for_buffer(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &App,
|
||||
) -> Option<(Entity<Worktree>, Arc<Path>)> {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let worktree_id = file.worktree_id(cx);
|
||||
let path = file.path().clone();
|
||||
let worktree = self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)?;
|
||||
Some((worktree, path))
|
||||
}
|
||||
|
||||
pub fn create_buffer(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<Buffer>>> {
|
||||
match &self.state {
|
||||
BufferStoreState::Local(this) => this.create_buffer(cx),
|
||||
|
|
|
@ -91,7 +91,7 @@ impl Manager {
|
|||
for (id, repository) in project.repositories(cx) {
|
||||
repositories.push(proto::RejoinRepository {
|
||||
id: id.to_proto(),
|
||||
scan_id: repository.read(cx).completed_scan_id as u64,
|
||||
scan_id: repository.read(cx).scan_id,
|
||||
});
|
||||
}
|
||||
for worktree in project.worktrees(cx) {
|
||||
|
|
|
@ -339,7 +339,7 @@ impl DapStore {
|
|||
local_store.toolchain_store.clone(),
|
||||
local_store.environment.update(cx, |env, cx| {
|
||||
let worktree = worktree.read(cx);
|
||||
env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx)
|
||||
env.get_environment(worktree.abs_path().into(), cx)
|
||||
}),
|
||||
);
|
||||
let session_id = local_store.next_session_id();
|
||||
|
@ -407,7 +407,7 @@ impl DapStore {
|
|||
local_store.toolchain_store.clone(),
|
||||
local_store.environment.update(cx, |env, cx| {
|
||||
let worktree = worktree.read(cx);
|
||||
env.get_environment(Some(worktree.id()), Some(worktree.abs_path()), cx)
|
||||
env.get_environment(Some(worktree.abs_path()), cx)
|
||||
}),
|
||||
);
|
||||
let session_id = local_store.next_session_id();
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
use futures::{FutureExt, future::Shared};
|
||||
use futures::{
|
||||
FutureExt,
|
||||
future::{Shared, WeakShared},
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
|
||||
use settings::Settings as _;
|
||||
use worktree::WorktreeId;
|
||||
|
||||
use crate::{
|
||||
project_settings::{DirenvSettings, ProjectSettings},
|
||||
|
@ -13,10 +15,9 @@ use crate::{
|
|||
};
|
||||
|
||||
pub struct ProjectEnvironment {
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
cli_environment: Option<HashMap<String, String>>,
|
||||
environments: HashMap<WorktreeId, Shared<Task<Option<HashMap<String, String>>>>>,
|
||||
environment_error_messages: HashMap<WorktreeId, EnvironmentErrorMessage>,
|
||||
environments: HashMap<Arc<Path>, WeakShared<Task<Option<HashMap<String, String>>>>>,
|
||||
environment_error_messages: HashMap<Arc<Path>, EnvironmentErrorMessage>,
|
||||
}
|
||||
|
||||
pub enum ProjectEnvironmentEvent {
|
||||
|
@ -33,14 +34,15 @@ impl ProjectEnvironment {
|
|||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
cx.subscribe(worktree_store, |this: &mut Self, _, event, _| {
|
||||
if let WorktreeStoreEvent::WorktreeRemoved(_, id) = event {
|
||||
this.remove_worktree_environment(*id);
|
||||
if let WorktreeStoreEvent::WorktreeRemoved(_, _) = event {
|
||||
this.environments.retain(|_, weak| weak.upgrade().is_some());
|
||||
this.environment_error_messages
|
||||
.retain(|abs_path, _| this.environments.contains_key(abs_path));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
worktree_store: worktree_store.clone(),
|
||||
cli_environment,
|
||||
environments: Default::default(),
|
||||
environment_error_messages: Default::default(),
|
||||
|
@ -48,11 +50,6 @@ impl ProjectEnvironment {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn remove_worktree_environment(&mut self, worktree_id: WorktreeId) {
|
||||
self.environment_error_messages.remove(&worktree_id);
|
||||
self.environments.remove(&worktree_id);
|
||||
}
|
||||
|
||||
/// Returns the inherited CLI environment, if this project was opened from the Zed CLI.
|
||||
pub(crate) fn get_cli_environment(&self) -> Option<HashMap<String, String>> {
|
||||
if let Some(mut env) = self.cli_environment.clone() {
|
||||
|
@ -67,28 +64,22 @@ impl ProjectEnvironment {
|
|||
/// environment errors associated with this project environment.
|
||||
pub(crate) fn environment_errors(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&WorktreeId, &EnvironmentErrorMessage)> {
|
||||
) -> impl Iterator<Item = (&Arc<Path>, &EnvironmentErrorMessage)> {
|
||||
self.environment_error_messages.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_environment_error(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.environment_error_messages.remove(&worktree_id);
|
||||
pub(crate) fn remove_environment_error(&mut self, abs_path: &Path, cx: &mut Context<Self>) {
|
||||
self.environment_error_messages.remove(abs_path);
|
||||
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated);
|
||||
}
|
||||
|
||||
/// Returns the project environment, if possible.
|
||||
/// If the project was opened from the CLI, then the inherited CLI environment is returned.
|
||||
/// If it wasn't opened from the CLI, and a worktree is given, then a shell is spawned in
|
||||
/// the worktree's path, to get environment variables as if the user has `cd`'d into
|
||||
/// the worktrees path.
|
||||
/// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in
|
||||
/// that directory, to get environment variables as if the user has `cd`'d there.
|
||||
pub(crate) fn get_environment(
|
||||
&mut self,
|
||||
worktree_id: Option<WorktreeId>,
|
||||
worktree_abs_path: Option<Arc<Path>>,
|
||||
abs_path: Option<Arc<Path>>,
|
||||
cx: &Context<Self>,
|
||||
) -> Shared<Task<Option<HashMap<String, String>>>> {
|
||||
if cfg!(any(test, feature = "test-support")) {
|
||||
|
@ -111,74 +102,26 @@ impl ProjectEnvironment {
|
|||
.shared();
|
||||
}
|
||||
|
||||
let Some((worktree_id, worktree_abs_path)) = worktree_id.zip(worktree_abs_path) else {
|
||||
let Some(abs_path) = abs_path else {
|
||||
return Task::ready(None).shared();
|
||||
};
|
||||
|
||||
if self
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
.map(|w| !w.read(cx).is_local())
|
||||
.unwrap_or(true)
|
||||
if let Some(existing) = self
|
||||
.environments
|
||||
.get(&abs_path)
|
||||
.and_then(|weak| weak.upgrade())
|
||||
{
|
||||
return Task::ready(None).shared();
|
||||
}
|
||||
|
||||
if let Some(task) = self.environments.get(&worktree_id) {
|
||||
task.clone()
|
||||
existing
|
||||
} else {
|
||||
let task = self
|
||||
.get_worktree_env(worktree_id, worktree_abs_path, cx)
|
||||
.shared();
|
||||
self.environments.insert(worktree_id, task.clone());
|
||||
task
|
||||
let env = get_directory_env(abs_path.clone(), cx).shared();
|
||||
self.environments.insert(
|
||||
abs_path.clone(),
|
||||
env.downgrade()
|
||||
.expect("environment task has not been polled yet"),
|
||||
);
|
||||
env
|
||||
}
|
||||
}
|
||||
|
||||
fn get_worktree_env(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
worktree_abs_path: Arc<Path>,
|
||||
cx: &Context<Self>,
|
||||
) -> Task<Option<HashMap<String, String>>> {
|
||||
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (mut shell_env, error_message) = cx
|
||||
.background_spawn({
|
||||
let worktree_abs_path = worktree_abs_path.clone();
|
||||
async move {
|
||||
load_worktree_shell_environment(&worktree_abs_path, &load_direnv).await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(shell_env) = shell_env.as_mut() {
|
||||
let path = shell_env
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables shell launched in {:?}. PATH={:?}",
|
||||
worktree_abs_path,
|
||||
path
|
||||
);
|
||||
|
||||
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
|
||||
}
|
||||
|
||||
if let Some(error) = error_message {
|
||||
this.update(cx, |this, cx| {
|
||||
this.environment_error_messages.insert(worktree_id, error);
|
||||
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
shell_env
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn set_origin_marker(env: &mut HashMap<String, String>, origin: EnvironmentOrigin) {
|
||||
|
@ -210,25 +153,25 @@ impl EnvironmentErrorMessage {
|
|||
}
|
||||
}
|
||||
|
||||
async fn load_worktree_shell_environment(
|
||||
worktree_abs_path: &Path,
|
||||
async fn load_directory_shell_environment(
|
||||
abs_path: &Path,
|
||||
load_direnv: &DirenvSettings,
|
||||
) -> (
|
||||
Option<HashMap<String, String>>,
|
||||
Option<EnvironmentErrorMessage>,
|
||||
) {
|
||||
match smol::fs::metadata(worktree_abs_path).await {
|
||||
match smol::fs::metadata(abs_path).await {
|
||||
Ok(meta) => {
|
||||
let dir = if meta.is_dir() {
|
||||
worktree_abs_path
|
||||
} else if let Some(parent) = worktree_abs_path.parent() {
|
||||
abs_path
|
||||
} else if let Some(parent) = abs_path.parent() {
|
||||
parent
|
||||
} else {
|
||||
return (
|
||||
None,
|
||||
Some(EnvironmentErrorMessage(format!(
|
||||
"Failed to load shell environment in {}: not a directory",
|
||||
worktree_abs_path.display()
|
||||
abs_path.display()
|
||||
))),
|
||||
);
|
||||
};
|
||||
|
@ -239,7 +182,7 @@ async fn load_worktree_shell_environment(
|
|||
None,
|
||||
Some(EnvironmentErrorMessage(format!(
|
||||
"Failed to load shell environment in {}: {}",
|
||||
worktree_abs_path.display(),
|
||||
abs_path.display(),
|
||||
err
|
||||
))),
|
||||
),
|
||||
|
@ -387,3 +330,43 @@ async fn load_shell_environment(
|
|||
|
||||
(Some(parsed_env), direnv_error)
|
||||
}
|
||||
|
||||
fn get_directory_env(
|
||||
abs_path: Arc<Path>,
|
||||
cx: &Context<ProjectEnvironment>,
|
||||
) -> Task<Option<HashMap<String, String>>> {
|
||||
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (mut shell_env, error_message) = cx
|
||||
.background_spawn({
|
||||
let abs_path = abs_path.clone();
|
||||
async move { load_directory_shell_environment(&abs_path, &load_direnv).await }
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(shell_env) = shell_env.as_mut() {
|
||||
let path = shell_env
|
||||
.get("PATH")
|
||||
.map(|path| path.as_str())
|
||||
.unwrap_or_default();
|
||||
log::info!(
|
||||
"using project environment variables shell launched in {:?}. PATH={:?}",
|
||||
abs_path,
|
||||
path
|
||||
);
|
||||
|
||||
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
|
||||
}
|
||||
|
||||
if let Some(error) = error_message {
|
||||
this.update(cx, |this, cx| {
|
||||
this.environment_error_messages.insert(abs_path, error);
|
||||
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
shell_env
|
||||
})
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,21 +3,21 @@ use git::status::GitSummary;
|
|||
use std::{ops::Deref, path::Path};
|
||||
use sum_tree::Cursor;
|
||||
use text::Bias;
|
||||
use worktree::{
|
||||
Entry, PathProgress, PathTarget, ProjectEntryId, RepositoryEntry, StatusEntry, Traversal,
|
||||
};
|
||||
use worktree::{Entry, PathProgress, PathTarget, Traversal};
|
||||
|
||||
use super::{RepositoryId, RepositorySnapshot, StatusEntry};
|
||||
|
||||
/// Walks the worktree entries and their associated git statuses.
|
||||
pub struct GitTraversal<'a> {
|
||||
traversal: Traversal<'a>,
|
||||
current_entry_summary: Option<GitSummary>,
|
||||
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
repo_location: Option<(ProjectEntryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
|
||||
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
|
||||
repo_location: Option<(RepositoryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
|
||||
}
|
||||
|
||||
impl<'a> GitTraversal<'a> {
|
||||
pub fn new(
|
||||
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
|
||||
traversal: Traversal<'a>,
|
||||
) -> GitTraversal<'a> {
|
||||
let mut this = GitTraversal {
|
||||
|
@ -46,8 +46,8 @@ impl<'a> GitTraversal<'a> {
|
|||
.repo_snapshots
|
||||
.values()
|
||||
.filter_map(|repo_snapshot| {
|
||||
let relative_path = repo_snapshot.relativize_abs_path(&abs_path)?;
|
||||
Some((repo_snapshot, relative_path))
|
||||
let repo_path = repo_snapshot.abs_path_to_repo_path(&abs_path)?;
|
||||
Some((repo_snapshot, repo_path))
|
||||
})
|
||||
.max_by_key(|(repo, _)| repo.work_directory_abs_path.clone())
|
||||
else {
|
||||
|
@ -61,12 +61,9 @@ impl<'a> GitTraversal<'a> {
|
|||
.repo_location
|
||||
.as_ref()
|
||||
.map(|(prev_repo_id, _)| *prev_repo_id)
|
||||
!= Some(repo.work_directory_id())
|
||||
!= Some(repo.id)
|
||||
{
|
||||
self.repo_location = Some((
|
||||
repo.work_directory_id(),
|
||||
repo.statuses_by_path.cursor::<PathProgress>(&()),
|
||||
));
|
||||
self.repo_location = Some((repo.id, repo.statuses_by_path.cursor::<PathProgress>(&())));
|
||||
}
|
||||
|
||||
let Some((_, statuses)) = &mut self.repo_location else {
|
||||
|
@ -148,7 +145,7 @@ pub struct ChildEntriesGitIter<'a> {
|
|||
|
||||
impl<'a> ChildEntriesGitIter<'a> {
|
||||
pub fn new(
|
||||
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
repo_snapshots: &'a HashMap<RepositoryId, RepositorySnapshot>,
|
||||
worktree_snapshot: &'a worktree::Snapshot,
|
||||
parent_path: &'a Path,
|
||||
) -> Self {
|
||||
|
@ -771,7 +768,7 @@ mod tests {
|
|||
|
||||
#[track_caller]
|
||||
fn check_git_statuses(
|
||||
repo_snapshots: &HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
repo_snapshots: &HashMap<RepositoryId, RepositorySnapshot>,
|
||||
worktree_snapshot: &worktree::Snapshot,
|
||||
expected_statuses: &[(&Path, GitSummary)],
|
||||
) {
|
||||
|
|
|
@ -8078,9 +8078,7 @@ impl LspStore {
|
|||
});
|
||||
|
||||
if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) {
|
||||
environment.update(cx, |env, cx| {
|
||||
env.get_environment(worktree_id, worktree_abs_path, cx)
|
||||
})
|
||||
environment.update(cx, |env, cx| env.get_environment(worktree_abs_path, cx))
|
||||
} else {
|
||||
Task::ready(None).shared()
|
||||
}
|
||||
|
@ -9864,13 +9862,10 @@ impl LocalLspAdapterDelegate {
|
|||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) -> Arc<Self> {
|
||||
let (worktree_id, worktree_abs_path) = {
|
||||
let worktree = worktree.read(cx);
|
||||
(worktree.id(), worktree.abs_path())
|
||||
};
|
||||
let worktree_abs_path = worktree.read(cx).abs_path();
|
||||
|
||||
let load_shell_env_task = environment.update(cx, |env, cx| {
|
||||
env.get_environment(Some(worktree_id), Some(worktree_abs_path), cx)
|
||||
env.get_environment(Some(worktree_abs_path), cx)
|
||||
});
|
||||
|
||||
Arc::new(Self {
|
||||
|
|
|
@ -24,7 +24,7 @@ mod direnv;
|
|||
mod environment;
|
||||
use buffer_diff::BufferDiff;
|
||||
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
|
||||
use git_store::{GitEvent, Repository};
|
||||
use git_store::{Repository, RepositoryId};
|
||||
pub mod search_history;
|
||||
mod yarn;
|
||||
|
||||
|
@ -300,8 +300,6 @@ pub enum Event {
|
|||
RevealInProjectPanel(ProjectEntryId),
|
||||
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
|
||||
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
|
||||
GitStateUpdated,
|
||||
ActiveRepositoryChanged,
|
||||
}
|
||||
|
||||
pub enum DebugAdapterClientState {
|
||||
|
@ -924,7 +922,6 @@ impl Project {
|
|||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&git_store, Self::on_git_store_event).detach();
|
||||
|
||||
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
||||
|
||||
|
@ -1064,13 +1061,7 @@ impl Project {
|
|||
});
|
||||
|
||||
let git_store = cx.new(|cx| {
|
||||
GitStore::ssh(
|
||||
&worktree_store,
|
||||
buffer_store.clone(),
|
||||
environment.clone(),
|
||||
ssh_proto.clone(),
|
||||
cx,
|
||||
)
|
||||
GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx)
|
||||
});
|
||||
|
||||
cx.subscribe(&ssh, Self::on_ssh_event).detach();
|
||||
|
@ -1655,13 +1646,13 @@ impl Project {
|
|||
pub fn shell_environment_errors<'a>(
|
||||
&'a self,
|
||||
cx: &'a App,
|
||||
) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
|
||||
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
|
||||
self.environment.read(cx).environment_errors()
|
||||
}
|
||||
|
||||
pub fn remove_environment_error(&mut self, worktree_id: WorktreeId, cx: &mut Context<Self>) {
|
||||
pub fn remove_environment_error(&mut self, abs_path: &Path, cx: &mut Context<Self>) {
|
||||
self.environment.update(cx, |environment, cx| {
|
||||
environment.remove_environment_error(worktree_id, cx);
|
||||
environment.remove_environment_error(abs_path, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2760,19 +2751,6 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_git_store_event(
|
||||
&mut self,
|
||||
_: Entity<GitStore>,
|
||||
event: &GitEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
GitEvent::GitStateUpdated => cx.emit(Event::GitStateUpdated),
|
||||
GitEvent::ActiveRepositoryChanged => cx.emit(Event::ActiveRepositoryChanged),
|
||||
GitEvent::FileSystemUpdated | GitEvent::IndexWriteError(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ssh_event(
|
||||
&mut self,
|
||||
_: Entity<SshRemoteClient>,
|
||||
|
@ -4794,7 +4772,7 @@ impl Project {
|
|||
self.git_store.read(cx).active_repository()
|
||||
}
|
||||
|
||||
pub fn repositories<'a>(&self, cx: &'a App) -> &'a HashMap<ProjectEntryId, Entity<Repository>> {
|
||||
pub fn repositories<'a>(&self, cx: &'a App) -> &'a HashMap<RepositoryId, Entity<Repository>> {
|
||||
self.git_store.read(cx).repositories()
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -298,7 +298,7 @@ fn local_task_context_for_location(
|
|||
let worktree_abs_path = worktree_abs_path.clone();
|
||||
let project_env = environment
|
||||
.update(cx, |environment, cx| {
|
||||
environment.get_environment(worktree_id, worktree_abs_path.clone(), cx)
|
||||
environment.get_environment(worktree_abs_path.clone(), cx)
|
||||
})
|
||||
.ok()?
|
||||
.await;
|
||||
|
|
|
@ -331,11 +331,7 @@ impl LocalToolchainStore {
|
|||
cx.spawn(async move |cx| {
|
||||
let project_env = environment
|
||||
.update(cx, |environment, cx| {
|
||||
environment.get_environment(
|
||||
Some(path.worktree_id),
|
||||
Some(Arc::from(abs_path.as_path())),
|
||||
cx,
|
||||
)
|
||||
environment.get_environment(Some(root.clone()), cx)
|
||||
})
|
||||
.ok()?
|
||||
.await;
|
||||
|
|
|
@ -29,7 +29,8 @@ use language::DiagnosticSeverity;
|
|||
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use project::{
|
||||
Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
|
||||
ProjectPath, Worktree, WorktreeId, git_store::git_traversal::ChildEntriesGitIter,
|
||||
ProjectPath, Worktree, WorktreeId,
|
||||
git_store::{GitStoreEvent, git_traversal::ChildEntriesGitIter},
|
||||
relativize_path,
|
||||
};
|
||||
use project_panel_settings::{
|
||||
|
@ -298,6 +299,7 @@ impl ProjectPanel {
|
|||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
let project = workspace.project().clone();
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
let project_panel = cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
|
||||
|
@ -306,6 +308,18 @@ impl ProjectPanel {
|
|||
this.hide_scrollbar(window, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&git_store, |this, _, event, cx| match event {
|
||||
GitStoreEvent::RepositoryUpdated(_, _, _)
|
||||
| GitStoreEvent::RepositoryAdded(_)
|
||||
| GitStoreEvent::RepositoryRemoved(_) => {
|
||||
this.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&project, |this, project, event, cx| match event {
|
||||
project::Event::ActiveEntryChanged(Some(entry_id)) => {
|
||||
if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
|
||||
|
@ -335,9 +349,7 @@ impl ProjectPanel {
|
|||
this.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::GitStateUpdated
|
||||
| project::Event::ActiveRepositoryChanged
|
||||
| project::Event::WorktreeUpdatedEntries(_, _)
|
||||
project::Event::WorktreeUpdatedEntries(_, _)
|
||||
| project::Event::WorktreeAdded(_)
|
||||
| project::Event::WorktreeOrderChanged => {
|
||||
this.update_visible_entries(None, cx);
|
||||
|
|
|
@ -1937,7 +1937,7 @@ message Entry {
|
|||
}
|
||||
|
||||
message RepositoryEntry {
|
||||
uint64 work_directory_id = 1;
|
||||
uint64 repository_id = 1;
|
||||
reserved 2;
|
||||
repeated StatusEntry updated_statuses = 3;
|
||||
repeated string removed_statuses = 4;
|
||||
|
@ -1955,6 +1955,7 @@ message UpdateRepository {
|
|||
repeated string removed_statuses = 7;
|
||||
repeated string current_merge_conflicts = 8;
|
||||
uint64 scan_id = 9;
|
||||
bool is_last_update = 10;
|
||||
}
|
||||
|
||||
message RemoveRepository {
|
||||
|
@ -2247,7 +2248,7 @@ message OpenUncommittedDiffResponse {
|
|||
message SetIndexText {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string path = 4;
|
||||
optional string text = 5;
|
||||
}
|
||||
|
@ -3356,7 +3357,7 @@ message GetPanicFiles {
|
|||
message GitShow {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string commit = 4;
|
||||
}
|
||||
|
||||
|
@ -3371,7 +3372,7 @@ message GitCommitDetails {
|
|||
message LoadCommitDiff {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string commit = 4;
|
||||
}
|
||||
|
||||
|
@ -3388,7 +3389,7 @@ message CommitFile {
|
|||
message GitReset {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string commit = 4;
|
||||
ResetMode mode = 5;
|
||||
enum ResetMode {
|
||||
|
@ -3400,7 +3401,7 @@ message GitReset {
|
|||
message GitCheckoutFiles {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string commit = 4;
|
||||
repeated string paths = 5;
|
||||
}
|
||||
|
@ -3455,21 +3456,21 @@ message RegisterBufferWithLanguageServers{
|
|||
message Stage {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
repeated string paths = 4;
|
||||
}
|
||||
|
||||
message Unstage {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
repeated string paths = 4;
|
||||
}
|
||||
|
||||
message Commit {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
optional string name = 4;
|
||||
optional string email = 5;
|
||||
string message = 6;
|
||||
|
@ -3478,13 +3479,13 @@ message Commit {
|
|||
message OpenCommitMessageBuffer {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
}
|
||||
|
||||
message Push {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string remote_name = 4;
|
||||
string branch_name = 5;
|
||||
optional PushOptions options = 6;
|
||||
|
@ -3499,14 +3500,14 @@ message Push {
|
|||
message Fetch {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
uint64 askpass_id = 4;
|
||||
}
|
||||
|
||||
message GetRemotes {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
optional string branch_name = 4;
|
||||
}
|
||||
|
||||
|
@ -3521,7 +3522,7 @@ message GetRemotesResponse {
|
|||
message Pull {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string remote_name = 4;
|
||||
string branch_name = 5;
|
||||
uint64 askpass_id = 6;
|
||||
|
@ -3535,7 +3536,7 @@ message RemoteMessageResponse {
|
|||
message AskPassRequest {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
uint64 askpass_id = 4;
|
||||
string prompt = 5;
|
||||
}
|
||||
|
@ -3547,27 +3548,27 @@ message AskPassResponse {
|
|||
message GitGetBranches {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
}
|
||||
|
||||
message GitCreateBranch {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string branch_name = 4;
|
||||
}
|
||||
|
||||
message GitChangeBranch {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
string branch_name = 4;
|
||||
}
|
||||
|
||||
message CheckForPushedCommits {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
}
|
||||
|
||||
message CheckForPushedCommitsResponse {
|
||||
|
@ -3577,7 +3578,7 @@ message CheckForPushedCommitsResponse {
|
|||
message GitDiff {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 work_directory_id = 3;
|
||||
uint64 repository_id = 3;
|
||||
DiffType diff_type = 4;
|
||||
|
||||
enum DiffType {
|
||||
|
|
|
@ -834,7 +834,7 @@ pub fn split_worktree_update(mut message: UpdateWorktree) -> impl Iterator<Item
|
|||
let removed_statuses_limit = cmp::min(repo.removed_statuses.len(), limit);
|
||||
|
||||
updated_repositories.push(RepositoryEntry {
|
||||
work_directory_id: repo.work_directory_id,
|
||||
repository_id: repo.repository_id,
|
||||
branch_summary: repo.branch_summary.clone(),
|
||||
updated_statuses: repo
|
||||
.updated_statuses
|
||||
|
@ -885,26 +885,34 @@ pub fn split_repository_update(
|
|||
) -> impl Iterator<Item = UpdateRepository> {
|
||||
let mut updated_statuses_iter = mem::take(&mut update.updated_statuses).into_iter().fuse();
|
||||
let mut removed_statuses_iter = mem::take(&mut update.removed_statuses).into_iter().fuse();
|
||||
let mut is_first = true;
|
||||
std::iter::from_fn(move || {
|
||||
let updated_statuses = updated_statuses_iter
|
||||
.by_ref()
|
||||
.take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE)
|
||||
.collect::<Vec<_>>();
|
||||
let removed_statuses = removed_statuses_iter
|
||||
.by_ref()
|
||||
.take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE)
|
||||
.collect::<Vec<_>>();
|
||||
if updated_statuses.is_empty() && removed_statuses.is_empty() && !is_first {
|
||||
return None;
|
||||
std::iter::from_fn({
|
||||
let update = update.clone();
|
||||
move || {
|
||||
let updated_statuses = updated_statuses_iter
|
||||
.by_ref()
|
||||
.take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE)
|
||||
.collect::<Vec<_>>();
|
||||
let removed_statuses = removed_statuses_iter
|
||||
.by_ref()
|
||||
.take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE)
|
||||
.collect::<Vec<_>>();
|
||||
if updated_statuses.is_empty() && removed_statuses.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(UpdateRepository {
|
||||
updated_statuses,
|
||||
removed_statuses,
|
||||
is_last_update: false,
|
||||
..update.clone()
|
||||
})
|
||||
}
|
||||
is_first = false;
|
||||
Some(UpdateRepository {
|
||||
updated_statuses,
|
||||
removed_statuses,
|
||||
..update.clone()
|
||||
})
|
||||
})
|
||||
.chain([UpdateRepository {
|
||||
updated_statuses: Vec::new(),
|
||||
removed_statuses: Vec::new(),
|
||||
is_last_update: true,
|
||||
..update
|
||||
}])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -28,6 +28,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
#[cfg(not(windows))]
|
||||
use unindent::Unindent as _;
|
||||
use util::{path, separator};
|
||||
|
||||
|
@ -1203,6 +1204,8 @@ async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestA
|
|||
});
|
||||
}
|
||||
|
||||
// TODO: this test fails on Windows.
|
||||
#[cfg(not(windows))]
|
||||
#[gpui::test]
|
||||
async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
let text_2 = "
|
||||
|
@ -1379,7 +1382,8 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
|||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.current_branch()
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone()
|
||||
})
|
||||
|
@ -1418,7 +1422,8 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
|
|||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.current_branch()
|
||||
.branch
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone()
|
||||
})
|
||||
|
|
|
@ -317,10 +317,18 @@ where
|
|||
))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: K) {
|
||||
self.0.insert(key, ());
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &K) -> bool {
|
||||
self.0.remove(key).is_some()
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, iter: impl IntoIterator<Item = K>) {
|
||||
self.0.extend(iter.into_iter().map(|key| (key, ())));
|
||||
}
|
||||
|
|
|
@ -522,7 +522,7 @@ impl TitleBar {
|
|||
pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
let repository = self.project.read(cx).active_repository(cx)?;
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let branch_name = repository.read(cx).current_branch()?.name.clone();
|
||||
let branch_name = repository.read(cx).branch.as_ref()?.name.clone();
|
||||
let branch_name = util::truncate_and_trailoff(&branch_name, MAX_BRANCH_NAME_LENGTH);
|
||||
Some(
|
||||
Button::new("project_branch_trigger", branch_name)
|
||||
|
|
|
@ -783,6 +783,7 @@ mod test {
|
|||
async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, false).await;
|
||||
cx.cx.set_state("ˇone one one one");
|
||||
cx.run_until_parked();
|
||||
cx.simulate_keystrokes("cmd-f");
|
||||
cx.run_until_parked();
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ fs.workspace = true
|
|||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
git_hosting_providers.workspace = true
|
||||
gpui.workspace = true
|
||||
ignore.workspace = true
|
||||
language.workspace = true
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,15 +1,10 @@
|
|||
use crate::{
|
||||
Entry, EntryKind, Event, PathChange, StatusEntry, WorkDirectory, Worktree, WorktreeModelHandle,
|
||||
Entry, EntryKind, Event, PathChange, WorkDirectory, Worktree, WorktreeModelHandle,
|
||||
worktree_settings::WorktreeSettings,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
|
||||
use git::{
|
||||
GITIGNORE,
|
||||
repository::RepoPath,
|
||||
status::{FileStatus, StatusCode, TrackedStatus},
|
||||
};
|
||||
use git2::RepositoryInitOptions;
|
||||
use git::GITIGNORE;
|
||||
use gpui::{AppContext as _, BorrowAppContext, Context, Task, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use postage::stream::Stream;
|
||||
|
@ -685,183 +680,6 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
|
|||
assert_eq!(read_dir_count_3 - read_dir_count_2, 2);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||
});
|
||||
});
|
||||
});
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
|
||||
"tree": {
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
"tracked-dir": {
|
||||
"tracked-file1": "",
|
||||
"ancestor-ignored-file1": "",
|
||||
},
|
||||
"ignored-dir": {
|
||||
"ignored-file1": ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_head_and_index_for_repo(
|
||||
path!("/root/tree/.git").as_ref(),
|
||||
&[
|
||||
(".gitignore".into(), "ignored-dir\n".into()),
|
||||
("tracked-dir/tracked-file1".into(), "".into()),
|
||||
],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
path!("/root/tree").as_ref(),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert_entry_git_state(tree, "tracked-dir/tracked-file1", None, false);
|
||||
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file1", None, false);
|
||||
assert_entry_git_state(tree, "ignored-dir/ignored-file1", None, true);
|
||||
});
|
||||
|
||||
fs.create_file(
|
||||
path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
fs.set_index_for_repo(
|
||||
path!("/root/tree/.git").as_ref(),
|
||||
&[
|
||||
(".gitignore".into(), "ignored-dir\n".into()),
|
||||
("tracked-dir/tracked-file1".into(), "".into()),
|
||||
("tracked-dir/tracked-file2".into(), "".into()),
|
||||
],
|
||||
);
|
||||
fs.create_file(
|
||||
path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
fs.create_file(
|
||||
path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert_entry_git_state(
|
||||
tree,
|
||||
"tracked-dir/tracked-file2",
|
||||
Some(StatusCode::Added),
|
||||
false,
|
||||
);
|
||||
assert_entry_git_state(tree, "tracked-dir/ancestor-ignored-file2", None, false);
|
||||
assert_entry_git_state(tree, "ignored-dir/ignored-file2", None, true);
|
||||
assert!(tree.entry_for_path(".git").unwrap().is_ignored);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_update_gitignore(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".git": {},
|
||||
".gitignore": "*.txt\n",
|
||||
"a.xml": "<a></a>",
|
||||
"b.txt": "Some text"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_and_index_for_repo(
|
||||
path!("/root/.git").as_ref(),
|
||||
&[
|
||||
(".gitignore".into(), "*.txt\n".into()),
|
||||
("a.xml".into(), "<a></a>".into()),
|
||||
],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
path!("/root").as_ref(),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.read_with(cx, |tree, _| {
|
||||
tree.as_local()
|
||||
.unwrap()
|
||||
.refresh_entries_for_paths(vec![Path::new("").into()])
|
||||
})
|
||||
.recv()
|
||||
.await;
|
||||
|
||||
// One file is unmodified, the other is ignored.
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert_entry_git_state(tree, "a.xml", None, false);
|
||||
assert_entry_git_state(tree, "b.txt", None, true);
|
||||
});
|
||||
|
||||
// Change the gitignore, and stage the newly non-ignored file.
|
||||
fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
|
||||
.await
|
||||
.unwrap();
|
||||
fs.set_index_for_repo(
|
||||
Path::new(path!("/root/.git")),
|
||||
&[
|
||||
(".gitignore".into(), "*.txt\n".into()),
|
||||
("a.xml".into(), "<a></a>".into()),
|
||||
("b.txt".into(), "Some text".into()),
|
||||
],
|
||||
);
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
assert_entry_git_state(tree, "a.xml", None, true);
|
||||
assert_entry_git_state(tree, "b.txt", Some(StatusCode::Added), false);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_write_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2106,655 +1924,6 @@ fn random_filename(rng: &mut impl Rng) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
|
||||
// a directory which some program has already open.
|
||||
// This is a limitation of the Windows.
|
||||
// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
let root = TempTree::new(json!({
|
||||
"projects": {
|
||||
"project1": {
|
||||
"a": "",
|
||||
"b": "",
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
let root_path = root.path();
|
||||
|
||||
let tree = Worktree::local(
|
||||
root_path,
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let repo = git_init(&root_path.join("projects/project1"));
|
||||
git_add("a", &repo);
|
||||
git_commit("init", &repo);
|
||||
std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let repo = tree.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory_abs_path,
|
||||
root_path.join("projects/project1")
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&"a".into()).map(|entry| entry.status),
|
||||
Some(StatusCode::Modified.worktree()),
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&"b".into()).map(|entry| entry.status),
|
||||
Some(FileStatus::Untracked),
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::rename(
|
||||
root_path.join("projects/project1"),
|
||||
root_path.join("projects/project2"),
|
||||
)
|
||||
.unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let repo = tree.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory_abs_path,
|
||||
root_path.join("projects/project2")
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&"a".into()).unwrap().status,
|
||||
StatusCode::Modified.worktree(),
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&"b".into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
|
||||
// you can't rename a directory which some program has already open. This is a
|
||||
// limitation of the Windows. See:
|
||||
// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)]
|
||||
async fn test_file_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
const IGNORE_RULE: &str = "**/target";
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"a.txt": "a",
|
||||
"b.txt": "bb",
|
||||
"c": {
|
||||
"d": {
|
||||
"e.txt": "eee"
|
||||
}
|
||||
},
|
||||
"f.txt": "ffff",
|
||||
"target": {
|
||||
"build_file": "???"
|
||||
},
|
||||
".gitignore": IGNORE_RULE
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const A_TXT: &str = "a.txt";
|
||||
const B_TXT: &str = "b.txt";
|
||||
const E_TXT: &str = "c/d/e.txt";
|
||||
const F_TXT: &str = "f.txt";
|
||||
const DOTGITIGNORE: &str = ".gitignore";
|
||||
const BUILD_FILE: &str = "target/build_file";
|
||||
|
||||
// Set up git repository before creating the worktree.
|
||||
let work_dir = root.path().join("project");
|
||||
let mut repo = git_init(work_dir.as_path());
|
||||
repo.add_ignore_rule(IGNORE_RULE).unwrap();
|
||||
git_add(A_TXT, &repo);
|
||||
git_add(E_TXT, &repo);
|
||||
git_add(DOTGITIGNORE, &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
|
||||
let tree = Worktree::local(
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let root_path = root.path();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry.work_directory_abs_path,
|
||||
root_path.join("project")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&B_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&F_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
});
|
||||
|
||||
// Modify a file in the working copy.
|
||||
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// The worktree detects that the file's git status has changed.
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&A_TXT.into()).unwrap().status,
|
||||
StatusCode::Modified.worktree(),
|
||||
);
|
||||
});
|
||||
|
||||
// Create a commit in the git repository.
|
||||
git_add(A_TXT, &repo);
|
||||
git_add(B_TXT, &repo);
|
||||
git_commit("Committing modified and added", &repo);
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// The worktree detects that the files' git status have changed.
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&F_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
assert_eq!(repo_entry.status_for_path(&B_TXT.into()), None);
|
||||
assert_eq!(repo_entry.status_for_path(&A_TXT.into()), None);
|
||||
});
|
||||
|
||||
// Modify files in the working copy and perform git operations on other files.
|
||||
git_reset(0, &repo);
|
||||
git_remove_index(Path::new(B_TXT), &repo);
|
||||
git_stash(&mut repo);
|
||||
std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
|
||||
std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Check that more complex repo changes are tracked
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo_entry.status_for_path(&A_TXT.into()), None);
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&B_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
assert_eq!(
|
||||
repo_entry.status_for_path(&E_TXT.into()).unwrap().status,
|
||||
StatusCode::Modified.worktree(),
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
|
||||
std::fs::remove_dir_all(work_dir.join("c")).unwrap();
|
||||
std::fs::write(
|
||||
work_dir.join(DOTGITIGNORE),
|
||||
[IGNORE_RULE, "f.txt"].join("\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
git_add(Path::new(DOTGITIGNORE), &repo);
|
||||
git_commit("Committing modified git ignore", &repo);
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let mut renamed_dir_name = "first_directory/second_directory";
|
||||
const RENAMED_FILE: &str = "rf.txt";
|
||||
|
||||
std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
|
||||
std::fs::write(
|
||||
work_dir.join(renamed_dir_name).join(RENAMED_FILE),
|
||||
"new-contents",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo_entry
|
||||
.status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
|
||||
.unwrap()
|
||||
.status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
});
|
||||
|
||||
renamed_dir_name = "new_first_directory/second_directory";
|
||||
|
||||
std::fs::rename(
|
||||
work_dir.join("first_directory"),
|
||||
work_dir.join("new_first_directory"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo_entry = snapshot.repositories.iter().next().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
repo_entry
|
||||
.status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
|
||||
.unwrap()
|
||||
.status,
|
||||
FileStatus::Untracked,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"a.txt": "a", // Modified
|
||||
"b.txt": "bb", // Added
|
||||
"c.txt": "ccc", // Unchanged
|
||||
"d.txt": "dddd", // Deleted
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
// Set up git repository before creating the worktree.
|
||||
let work_dir = root.path().join("project");
|
||||
let repo = git_init(work_dir.as_path());
|
||||
git_add("a.txt", &repo);
|
||||
git_add("c.txt", &repo);
|
||||
git_add("d.txt", &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
std::fs::remove_file(work_dir.join("d.txt")).unwrap();
|
||||
std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
|
||||
|
||||
let tree = Worktree::local(
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
[
|
||||
StatusEntry {
|
||||
repo_path: "a.txt".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "b.txt".into(),
|
||||
status: FileStatus::Untracked,
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "d.txt".into(),
|
||||
status: StatusCode::Deleted.worktree(),
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repository = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repository.status().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
[
|
||||
StatusEntry {
|
||||
repo_path: "a.txt".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "b.txt".into(),
|
||||
status: FileStatus::Untracked,
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "c.txt".into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
},
|
||||
StatusEntry {
|
||||
repo_path: "d.txt".into(),
|
||||
status: StatusCode::Deleted.worktree(),
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
git_add("a.txt", &repo);
|
||||
git_add("c.txt", &repo);
|
||||
git_remove_index(Path::new("d.txt"), &repo);
|
||||
git_commit("Another commit", &repo);
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
std::fs::remove_file(work_dir.join("a.txt")).unwrap();
|
||||
std::fs::remove_file(work_dir.join("b.txt")).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
// Deleting an untracked entry, b.txt, should leave no status
|
||||
// a.txt was tracked, and so should have a status
|
||||
assert_eq!(
|
||||
entries,
|
||||
[StatusEntry {
|
||||
repo_path: "a.txt".into(),
|
||||
status: StatusCode::Deleted.worktree(),
|
||||
}]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"sub": {},
|
||||
"a.txt": "",
|
||||
},
|
||||
}));
|
||||
|
||||
let work_dir = root.path().join("project");
|
||||
let repo = git_init(work_dir.as_path());
|
||||
// a.txt exists in HEAD and the working copy but is deleted in the index.
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
git_remove_index("a.txt".as_ref(), &repo);
|
||||
// `sub` is a nested git repository.
|
||||
let _sub = git_init(&work_dir.join("sub"));
|
||||
|
||||
let tree = Worktree::local(
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
let entries = repo.status().collect::<Vec<_>>();
|
||||
|
||||
// `sub` doesn't appear in our computed statuses.
|
||||
// a.txt appears with a combined `DA` status.
|
||||
assert_eq!(
|
||||
entries,
|
||||
[StatusEntry {
|
||||
repo_path: "a.txt".into(),
|
||||
status: TrackedStatus {
|
||||
index_status: StatusCode::Deleted,
|
||||
worktree_status: StatusCode::Added
|
||||
}
|
||||
.into(),
|
||||
}]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"my-repo": {
|
||||
// .git folder will go here
|
||||
"a.txt": "a",
|
||||
"sub-folder-1": {
|
||||
"sub-folder-2": {
|
||||
"c.txt": "cc",
|
||||
"d": {
|
||||
"e.txt": "eee"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
|
||||
const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
|
||||
|
||||
// Set up git repository before creating the worktree.
|
||||
let git_repo_work_dir = root.path().join("my-repo");
|
||||
let repo = git_init(git_repo_work_dir.as_path());
|
||||
git_add(C_TXT, &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
|
||||
// Open the worktree in subfolder
|
||||
let project_root = Path::new("my-repo/sub-folder-1/sub-folder-2");
|
||||
let tree = Worktree::local(
|
||||
root.path().join(project_root),
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
tree.flush_fs_events_in_root_git_repository(cx).await;
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Ensure that the git status is loaded correctly
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repositories.iter().count(), 1);
|
||||
let repo = snapshot.repositories.iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.work_directory_abs_path.canonicalize().unwrap(),
|
||||
root.path().join("my-repo").canonicalize().unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(repo.status_for_path(&C_TXT.into()), None);
|
||||
assert_eq!(
|
||||
repo.status_for_path(&E_TXT.into()).unwrap().status,
|
||||
FileStatus::Untracked
|
||||
);
|
||||
});
|
||||
|
||||
// Now we simulate FS events, but ONLY in the .git folder that's outside
|
||||
// of out project root.
|
||||
// Meaning: we don't produce any FS events for files inside the project.
|
||||
git_add(E_TXT, &repo);
|
||||
git_commit("Second commit", &repo);
|
||||
tree.flush_fs_events_in_root_git_repository(cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let repos = snapshot.repositories().iter().cloned().collect::<Vec<_>>();
|
||||
assert_eq!(repos.len(), 1);
|
||||
let repo_entry = repos.into_iter().next().unwrap();
|
||||
|
||||
assert!(snapshot.repositories.iter().next().is_some());
|
||||
|
||||
assert_eq!(repo_entry.status_for_path(&C_TXT.into()), None);
|
||||
assert_eq!(repo_entry.status_for_path(&E_TXT.into()), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_conflicted_cherry_pick(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let root = TempTree::new(json!({
|
||||
"project": {
|
||||
"a.txt": "a",
|
||||
},
|
||||
}));
|
||||
let root_path = root.path();
|
||||
|
||||
let tree = Worktree::local(
|
||||
root_path,
|
||||
true,
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let repo = git_init(&root_path.join("project"));
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("init", &repo);
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
git_branch("other-branch", &repo);
|
||||
git_checkout("refs/heads/other-branch", &repo);
|
||||
std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("capitalize", &repo);
|
||||
let commit = repo
|
||||
.head()
|
||||
.expect("Failed to get HEAD")
|
||||
.peel_to_commit()
|
||||
.expect("HEAD is not a commit");
|
||||
git_checkout("refs/heads/main", &repo);
|
||||
std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
|
||||
git_add("a.txt", &repo);
|
||||
git_commit("improve letter", &repo);
|
||||
git_cherry_pick(&commit, &repo);
|
||||
std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
|
||||
.expect("No CHERRY_PICK_HEAD");
|
||||
pretty_assertions::assert_eq!(
|
||||
git_status(&repo),
|
||||
collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
|
||||
);
|
||||
tree.flush_fs_events(cx).await;
|
||||
let conflicts = tree.update(cx, |tree, _| {
|
||||
let entry = tree.repositories.first().expect("No git entry").clone();
|
||||
entry
|
||||
.current_merge_conflicts
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
|
||||
|
||||
git_add("a.txt", &repo);
|
||||
// Attempt to manually simulate what `git cherry-pick --continue` would do.
|
||||
git_commit("whatevs", &repo);
|
||||
std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
|
||||
.expect("Failed to remove CHERRY_PICK_HEAD");
|
||||
pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
|
||||
tree.flush_fs_events(cx).await;
|
||||
let conflicts = tree.update(cx, |tree, _| {
|
||||
let entry = tree.repositories.first().expect("No git entry").clone();
|
||||
entry
|
||||
.current_merge_conflicts
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(conflicts, []);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
@ -2815,110 +1984,6 @@ fn test_unrelativize() {
|
|||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_init(path: &Path) -> git2::Repository {
|
||||
let mut init_opts = RepositoryInitOptions::new();
|
||||
init_opts.initial_head("main");
|
||||
git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
|
||||
let path = path.as_ref();
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.add_path(path).expect("Failed to add file");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_remove_index(path: &Path, repo: &git2::Repository) {
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.remove_path(path).expect("Failed to add file");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_commit(msg: &'static str, repo: &git2::Repository) {
|
||||
use git2::Signature;
|
||||
|
||||
let signature = Signature::now("test", "test@zed.dev").unwrap();
|
||||
let oid = repo.index().unwrap().write_tree().unwrap();
|
||||
let tree = repo.find_tree(oid).unwrap();
|
||||
if let Ok(head) = repo.head() {
|
||||
let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
|
||||
|
||||
let parent_commit = parent_obj.as_commit().unwrap();
|
||||
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
&[parent_commit],
|
||||
)
|
||||
.expect("Failed to commit with parent");
|
||||
} else {
|
||||
repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
|
||||
.expect("Failed to commit");
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
|
||||
repo.cherrypick(commit, None).expect("Failed to cherrypick");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_stash(repo: &mut git2::Repository) {
|
||||
use git2::Signature;
|
||||
|
||||
let signature = Signature::now("test", "test@zed.dev").unwrap();
|
||||
repo.stash_save(&signature, "N/A", None)
|
||||
.expect("Failed to stash");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_reset(offset: usize, repo: &git2::Repository) {
|
||||
let head = repo.head().expect("Couldn't get repo head");
|
||||
let object = head.peel(git2::ObjectType::Commit).unwrap();
|
||||
let commit = object.as_commit().unwrap();
|
||||
let new_head = commit
|
||||
.parents()
|
||||
.inspect(|parnet| {
|
||||
parnet.message();
|
||||
})
|
||||
.nth(offset)
|
||||
.expect("Not enough history");
|
||||
repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
|
||||
.expect("Could not reset");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_branch(name: &str, repo: &git2::Repository) {
|
||||
let head = repo
|
||||
.head()
|
||||
.expect("Couldn't get repo head")
|
||||
.peel_to_commit()
|
||||
.expect("HEAD is not a commit");
|
||||
repo.branch(name, &head, false).expect("Failed to commit");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_checkout(name: &str, repo: &git2::Repository) {
|
||||
repo.set_head(name).expect("Failed to set head");
|
||||
repo.checkout_head(None).expect("Failed to check out head");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
|
||||
repo.statuses(None)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|status| (status.path().unwrap().to_string(), status.status()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn check_worktree_entries(
|
||||
tree: &Worktree,
|
||||
|
@ -2974,34 +2039,3 @@ fn init_test(cx: &mut gpui::TestAppContext) {
|
|||
WorktreeSettings::register(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_entry_git_state(
|
||||
tree: &Worktree,
|
||||
path: &str,
|
||||
index_status: Option<StatusCode>,
|
||||
is_ignored: bool,
|
||||
) {
|
||||
let entry = tree.entry_for_path(path).expect("entry {path} not found");
|
||||
let repos = tree.repositories().iter().cloned().collect::<Vec<_>>();
|
||||
assert_eq!(repos.len(), 1);
|
||||
let repo_entry = repos.into_iter().next().unwrap();
|
||||
let status = repo_entry
|
||||
.status_for_path(&path.into())
|
||||
.map(|entry| entry.status);
|
||||
let expected = index_status.map(|index_status| {
|
||||
TrackedStatus {
|
||||
index_status,
|
||||
worktree_status: StatusCode::Unmodified,
|
||||
}
|
||||
.into()
|
||||
});
|
||||
assert_eq!(
|
||||
status, expected,
|
||||
"expected {path} to have git status: {expected:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
entry.is_ignored, is_ignored,
|
||||
"expected {path} to have is_ignored: {is_ignored}"
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue