Detailed changes
@@ -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,
+ };
+ };
+
+ let remote_url = backend.remote_url("origin");
+ let head_sha = backend.head_sha();
+ let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
+
+ GitState {
+ remote_url,
+ head_sha,
+ current_branch,
+ diff,
+ }
+ })
+ })
+ });
- // Extract git information
- let git_state = match repo_info {
+ let git_state = match git_state {
+ Some(git_state) => match git_state.ok() {
+ Some(git_state) => git_state.await.ok(),
+ None => None,
+ },
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();
-
- // Get diff asynchronously
- let diff = repo
- .diff(git::repository::DiffType::HeadToWorktree)
- .await
- .ok();
-
- Some(GitState {
- remote_url,
- head_sha,
- current_branch,
- diff,
- })
- }
};
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
+ })
+}
@@ -14,17 +14,20 @@ use fs::Fs;
use futures::{
FutureExt as _, StreamExt as _,
channel::{mpsc, oneshot},
- future::{self, OptionFuture, Shared},
+ future::{self, Shared},
};
use git::{
- BuildPermalinkParams, GitHostingProviderRegistry,
+ BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH,
blame::Blame,
parse_git_remote_url,
repository::{
Branch, CommitDetails, CommitDiff, CommitFile, DiffType, GitRepository,
GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
+ UpstreamTrackingStatus,
+ },
+ status::{
+ FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
},
- status::FileStatus,
};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
@@ -37,38 +40,40 @@ use language::{
use parking_lot::Mutex;
use rpc::{
AnyProtoClient, TypedEnvelope,
- proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset},
+ proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset, split_repository_update},
};
use serde::Deserialize;
-use settings::WorktreeId;
use std::{
- collections::{VecDeque, hash_map},
+ cmp::Ordering,
+ collections::{BTreeSet, VecDeque},
future::Future,
+ mem,
ops::Range,
path::{Path, PathBuf},
- sync::Arc,
+ sync::{
+ Arc,
+ atomic::{self, AtomicU64},
+ },
};
-use sum_tree::TreeSet;
-use text::BufferId;
-use util::{ResultExt, debug_panic, maybe};
+use sum_tree::{Edit, SumTree, TreeSet};
+use text::{Bias, BufferId};
+use util::{ResultExt, debug_panic};
use worktree::{
- File, PathKey, ProjectEntryId, RepositoryEntry, StatusEntry, UpdatedGitRepositoriesSet,
- Worktree, proto_to_branch,
+ File, PathKey, PathProgress, PathSummary, PathTarget, UpdatedGitRepositoriesSet, Worktree,
};
pub struct GitStore {
state: GitStoreState,
buffer_store: Entity<BufferStore>,
worktree_store: Entity<WorktreeStore>,
- repositories: HashMap<ProjectEntryId, Entity<Repository>>,
- active_repo_id: Option<ProjectEntryId>,
+ repositories: HashMap<RepositoryId, Entity<Repository>>,
+ active_repo_id: Option<RepositoryId>,
#[allow(clippy::type_complexity)]
loading_diffs:
HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
diffs: HashMap<BufferId, Entity<BufferDiffState>>,
- update_sender: mpsc::UnboundedSender<GitJob>,
shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
- _subscriptions: [Subscription; 2],
+ _subscriptions: Vec<Subscription>,
}
#[derive(Default)]
@@ -113,25 +118,25 @@ enum DiffKind {
enum GitStoreState {
Local {
- downstream_client: Option<LocalDownstreamState>,
- environment: Entity<ProjectEnvironment>,
+ next_repository_id: Arc<AtomicU64>,
+ downstream: Option<LocalDownstreamState>,
+ project_environment: Entity<ProjectEnvironment>,
fs: Arc<dyn Fs>,
},
Ssh {
upstream_client: AnyProtoClient,
upstream_project_id: ProjectId,
- downstream_client: Option<(AnyProtoClient, ProjectId)>,
- environment: Entity<ProjectEnvironment>,
+ downstream: Option<(AnyProtoClient, ProjectId)>,
},
Remote {
upstream_client: AnyProtoClient,
- project_id: ProjectId,
+ upstream_project_id: ProjectId,
},
}
enum DownstreamUpdate {
- UpdateRepository(RepositoryEntry),
- RemoveRepository(ProjectEntryId),
+ UpdateRepository(RepositorySnapshot),
+ RemoveRepository(RepositoryId),
}
struct LocalDownstreamState {
@@ -143,54 +148,145 @@ struct LocalDownstreamState {
#[derive(Clone)]
pub struct GitStoreCheckpoint {
- checkpoints_by_work_dir_abs_path: HashMap<PathBuf, GitRepositoryCheckpoint>,
+ checkpoints_by_work_dir_abs_path: HashMap<Arc<Path>, GitRepositoryCheckpoint>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct StatusEntry {
+ pub repo_path: RepoPath,
+ pub status: FileStatus,
+}
+
+impl StatusEntry {
+ fn to_proto(&self) -> proto::StatusEntry {
+ let simple_status = match self.status {
+ FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
+ FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
+ FileStatus::Tracked(TrackedStatus {
+ index_status,
+ worktree_status,
+ }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
+ worktree_status
+ } else {
+ index_status
+ }),
+ };
+
+ proto::StatusEntry {
+ repo_path: self.repo_path.as_ref().to_proto(),
+ simple_status,
+ status: Some(status_to_proto(self.status)),
+ }
+ }
+}
+
+impl TryFrom<proto::StatusEntry> for StatusEntry {
+ type Error = anyhow::Error;
+
+ fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
+ let repo_path = RepoPath(Arc::<Path>::from_proto(value.repo_path));
+ let status = status_from_proto(value.simple_status, value.status)?;
+ Ok(Self { repo_path, status })
+ }
+}
+
+impl sum_tree::Item for StatusEntry {
+ type Summary = PathSummary<GitSummary>;
+
+ fn summary(&self, _: &<Self::Summary as sum_tree::Summary>::Context) -> Self::Summary {
+ PathSummary {
+ max_path: self.repo_path.0.clone(),
+ item_summary: self.status.summary(),
+ }
+ }
+}
+
+impl sum_tree::KeyedItem for StatusEntry {
+ type Key = PathKey;
+
+ fn key(&self) -> Self::Key {
+ PathKey(self.repo_path.0.clone())
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RepositoryId(pub u64);
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct RepositorySnapshot {
+ pub id: RepositoryId,
+ pub merge_message: Option<SharedString>,
+ pub statuses_by_path: SumTree<StatusEntry>,
+ pub work_directory_abs_path: Arc<Path>,
+ pub branch: Option<Branch>,
+ pub merge_conflicts: TreeSet<RepoPath>,
+ pub merge_head_shas: Vec<SharedString>,
+ pub scan_id: u64,
}
pub struct Repository {
- pub repository_entry: RepositoryEntry,
- pub merge_message: Option<String>,
- pub completed_scan_id: usize,
+ snapshot: RepositorySnapshot,
commit_message_buffer: Option<Entity<Buffer>>,
git_store: WeakEntity<GitStore>,
- project_environment: Option<WeakEntity<ProjectEnvironment>>,
- pub worktree_id: Option<WorktreeId>,
- state: RepositoryState,
+ // For a local repository, holds paths that have had worktree events since the last status scan completed,
+ // and that should be examined during the next status scan.
+ paths_needing_status_update: BTreeSet<RepoPath>,
job_sender: mpsc::UnboundedSender<GitJob>,
askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
latest_askpass_id: u64,
}
+impl std::ops::Deref for Repository {
+ type Target = RepositorySnapshot;
+
+ fn deref(&self) -> &Self::Target {
+ &self.snapshot
+ }
+}
+
#[derive(Clone)]
-enum RepositoryState {
- Local(Arc<dyn GitRepository>),
+pub enum RepositoryState {
+ Local {
+ backend: Arc<dyn GitRepository>,
+ environment: Arc<HashMap<String, String>>,
+ },
Remote {
project_id: ProjectId,
client: AnyProtoClient,
- work_directory_id: ProjectEntryId,
},
}
+#[derive(Clone, Debug)]
+pub enum RepositoryEvent {
+ Updated,
+ MergeHeadsChanged,
+}
+
#[derive(Debug)]
-pub enum GitEvent {
- ActiveRepositoryChanged,
- FileSystemUpdated,
- GitStateUpdated,
+pub enum GitStoreEvent {
+ ActiveRepositoryChanged(Option<RepositoryId>),
+ RepositoryUpdated(RepositoryId, RepositoryEvent, bool),
+ RepositoryAdded(RepositoryId),
+ RepositoryRemoved(RepositoryId),
IndexWriteError(anyhow::Error),
}
+impl EventEmitter<RepositoryEvent> for Repository {}
+impl EventEmitter<GitStoreEvent> for GitStore {}
+
struct GitJob {
- job: Box<dyn FnOnce(&mut AsyncApp) -> Task<()>>,
+ job: Box<dyn FnOnce(RepositoryState, &mut AsyncApp) -> Task<()>>,
key: Option<GitJobKey>,
}
#[derive(PartialEq, Eq)]
enum GitJobKey {
WriteIndex(RepoPath),
- BatchReadIndex(ProjectEntryId),
+ BatchReadIndex,
+ RefreshStatuses,
+ ReloadGitState,
}
-impl EventEmitter<GitEvent> for GitStore {}
-
impl GitStore {
pub fn local(
worktree_store: &Entity<WorktreeStore>,
@@ -203,8 +299,9 @@ impl GitStore {
worktree_store.clone(),
buffer_store,
GitStoreState::Local {
- downstream_client: None,
- environment,
+ next_repository_id: Arc::new(AtomicU64::new(1)),
+ downstream: None,
+ project_environment: environment,
fs,
},
cx,
@@ -223,7 +320,7 @@ impl GitStore {
buffer_store,
GitStoreState::Remote {
upstream_client,
- project_id,
+ upstream_project_id: project_id,
},
cx,
)
@@ -232,7 +329,6 @@ impl GitStore {
pub fn ssh(
worktree_store: &Entity<WorktreeStore>,
buffer_store: Entity<BufferStore>,
- environment: Entity<ProjectEnvironment>,
upstream_client: AnyProtoClient,
cx: &mut Context<Self>,
) -> Self {
@@ -242,8 +338,7 @@ impl GitStore {
GitStoreState::Ssh {
upstream_client,
upstream_project_id: ProjectId(SSH_PROJECT_ID),
- downstream_client: None,
- environment,
+ downstream: None,
},
cx,
)
@@ -255,8 +350,7 @@ impl GitStore {
state: GitStoreState,
cx: &mut Context<Self>,
) -> Self {
- let update_sender = Self::spawn_git_worker(cx);
- let _subscriptions = [
+ let _subscriptions = vec![
cx.subscribe(&worktree_store, Self::on_worktree_store_event),
cx.subscribe(&buffer_store, Self::on_buffer_store_event),
];
@@ -267,7 +361,6 @@ impl GitStore {
worktree_store,
repositories: HashMap::default(),
active_repo_id: None,
- update_sender,
_subscriptions,
loading_diffs: HashMap::default(),
shared_diffs: HashMap::default(),
@@ -312,24 +405,27 @@ impl GitStore {
pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
match &mut self.state {
GitStoreState::Ssh {
- downstream_client, ..
+ downstream: downstream_client,
+ ..
} => {
for repo in self.repositories.values() {
- client
- .send(repo.read(cx).repository_entry.initial_update(project_id))
- .log_err();
+ let update = repo.read(cx).snapshot.initial_update(project_id);
+ for update in split_repository_update(update) {
+ client.send(update).log_err();
+ }
}
*downstream_client = Some((client, ProjectId(project_id)));
}
GitStoreState::Local {
- downstream_client, ..
+ downstream: downstream_client,
+ ..
} => {
let mut snapshots = HashMap::default();
let (updates_tx, mut updates_rx) = mpsc::unbounded();
for repo in self.repositories.values() {
updates_tx
.unbounded_send(DownstreamUpdate::UpdateRepository(
- repo.read(cx).repository_entry.clone(),
+ repo.read(cx).snapshot.clone(),
))
.ok();
}
@@ -342,17 +438,20 @@ impl GitStore {
while let Some(update) = updates_rx.next().await {
match update {
DownstreamUpdate::UpdateRepository(snapshot) => {
- if let Some(old_snapshot) =
- snapshots.get_mut(&snapshot.work_directory_id)
+ if let Some(old_snapshot) = snapshots.get_mut(&snapshot.id)
{
let update =
snapshot.build_update(old_snapshot, project_id);
*old_snapshot = snapshot;
- client.send(update)?;
+ for update in split_repository_update(update) {
+ client.send(update)?;
+ }
} else {
let update = snapshot.initial_update(project_id);
- client.send(update)?;
- snapshots.insert(snapshot.work_directory_id, snapshot);
+ for update in split_repository_update(update) {
+ client.send(update)?;
+ }
+ snapshots.insert(snapshot.id, snapshot);
}
}
DownstreamUpdate::RemoveRepository(id) => {
@@ -369,7 +468,8 @@ impl GitStore {
.ok();
this.update(cx, |this, _| {
if let GitStoreState::Local {
- downstream_client, ..
+ downstream: downstream_client,
+ ..
} = &mut this.state
{
downstream_client.take();
@@ -389,12 +489,14 @@ impl GitStore {
pub fn unshared(&mut self, _cx: &mut Context<Self>) {
match &mut self.state {
GitStoreState::Local {
- downstream_client, ..
+ downstream: downstream_client,
+ ..
} => {
downstream_client.take();
}
GitStoreState::Ssh {
- downstream_client, ..
+ downstream: downstream_client,
+ ..
} => {
downstream_client.take();
}
@@ -440,29 +542,32 @@ impl GitStore {
}
}
- let task = match self.loading_diffs.entry((buffer_id, DiffKind::Unstaged)) {
- hash_map::Entry::Occupied(e) => e.get().clone(),
- hash_map::Entry::Vacant(entry) => {
- let staged_text = self.state.load_staged_text(&buffer, &self.buffer_store, cx);
- entry
- .insert(
- cx.spawn(async move |this, cx| {
- Self::open_diff_internal(
- this,
- DiffKind::Unstaged,
- staged_text.await.map(DiffBasesChange::SetIndex),
- buffer,
- cx,
- )
- .await
- .map_err(Arc::new)
- })
- .shared(),
- )
- .clone()
- }
+ let Some((repo, repo_path)) =
+ self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
+ else {
+ return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
};
+ let task = self
+ .loading_diffs
+ .entry((buffer_id, DiffKind::Unstaged))
+ .or_insert_with(|| {
+ let staged_text = repo.read(cx).load_staged_text(buffer_id, repo_path, cx);
+ cx.spawn(async move |this, cx| {
+ Self::open_diff_internal(
+ this,
+ DiffKind::Unstaged,
+ staged_text.await.map(DiffBasesChange::SetIndex),
+ buffer,
+ cx,
+ )
+ .await
+ .map_err(Arc::new)
+ })
+ .shared()
+ })
+ .clone();
+
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
}
@@ -492,32 +597,26 @@ impl GitStore {
}
}
- let task = match self.loading_diffs.entry((buffer_id, DiffKind::Uncommitted)) {
- hash_map::Entry::Occupied(e) => e.get().clone(),
- hash_map::Entry::Vacant(entry) => {
- let changes = self
- .state
- .load_committed_text(&buffer, &self.buffer_store, cx);
-
- entry
- .insert(
- cx.spawn(async move |this, cx| {
- Self::open_diff_internal(
- this,
- DiffKind::Uncommitted,
- changes.await,
- buffer,
- cx,
- )
- .await
- .map_err(Arc::new)
- })
- .shared(),
- )
- .clone()
- }
+ let Some((repo, repo_path)) =
+ self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
+ else {
+ return Task::ready(Err(anyhow!("failed to find git repository for buffer")));
};
+ let task = self
+ .loading_diffs
+ .entry((buffer_id, DiffKind::Uncommitted))
+ .or_insert_with(|| {
+ let changes = repo.read(cx).load_committed_text(buffer_id, repo_path, cx);
+ cx.spawn(async move |this, cx| {
+ Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx)
+ .await
+ .map_err(Arc::new)
+ })
+ .shared()
+ })
+ .clone();
+
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
}
@@ -607,12 +706,7 @@ impl GitStore {
cx: &App,
) -> Option<FileStatus> {
let (repo, repo_path) = self.repository_and_path_for_project_path(project_path, cx)?;
- Some(
- repo.read(cx)
- .repository_entry
- .status_for_path(&repo_path)?
- .status,
- )
+ Some(repo.read(cx).status_for_path(&repo_path)?.status)
}
pub fn checkpoint(&self, cx: &App) -> Task<Result<GitStoreCheckpoint>> {
@@ -620,8 +714,7 @@ impl GitStore {
let mut checkpoints = Vec::new();
for repository in self.repositories.values() {
let repository = repository.read(cx);
- work_directory_abs_paths
- .push(repository.repository_entry.work_directory_abs_path.clone());
+ work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone());
checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?));
}
@@ -640,15 +733,7 @@ impl GitStore {
let repositories_by_work_dir_abs_path = self
.repositories
.values()
- .map(|repo| {
- (
- repo.read(cx)
- .repository_entry
- .work_directory_abs_path
- .clone(),
- repo,
- )
- })
+ .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
.collect::<HashMap<_, _>>();
let mut tasks = Vec::new();
@@ -674,15 +759,7 @@ impl GitStore {
let repositories_by_work_dir_abs_path = self
.repositories
.values()
- .map(|repo| {
- (
- repo.read(cx)
- .repository_entry
- .work_directory_abs_path
- .clone(),
- repo,
- )
- })
+ .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
.collect::<HashMap<_, _>>();
let mut tasks = Vec::new();
@@ -714,15 +791,7 @@ impl GitStore {
let repositories_by_work_directory_abs_path = self
.repositories
.values()
- .map(|repo| {
- (
- repo.read(cx)
- .repository_entry
- .work_directory_abs_path
- .clone(),
- repo,
- )
- })
+ .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo))
.collect::<HashMap<_, _>>();
let mut tasks = Vec::new();
@@ -748,60 +817,39 @@ impl GitStore {
cx: &App,
) -> Task<Result<Option<Blame>>> {
let buffer = buffer.read(cx);
- let Some(file) = File::from_dyn(buffer.file()) else {
- return Task::ready(Err(anyhow!("buffer has no file")));
+ let Some((repo, repo_path)) =
+ self.repository_and_path_for_buffer_id(buffer.remote_id(), cx)
+ else {
+ return Task::ready(Err(anyhow!("failed to find a git repository for buffer")));
};
-
- match file.worktree.clone().read(cx) {
- Worktree::Local(worktree) => {
- let worktree = worktree.snapshot();
- let blame_params = maybe!({
- let local_repo = match worktree.local_repo_containing_path(&file.path) {
- Some(repo_for_path) => repo_for_path,
- None => return Ok(None),
- };
-
- let relative_path = local_repo
- .relativize(&file.path)
- .context("failed to relativize buffer path")?;
-
- let repo = local_repo.repo().clone();
-
- let content = match version {
- Some(version) => buffer.rope_for_version(&version).clone(),
- None => buffer.as_rope().clone(),
- };
-
- anyhow::Ok(Some((repo, relative_path, content)))
- });
-
- cx.spawn(async move |_cx| {
- let Some((repo, relative_path, content)) = blame_params? else {
- return Ok(None);
- };
- repo.blame(relative_path.clone(), content)
- .await
- .with_context(|| format!("Failed to blame {:?}", relative_path.0))
- .map(Some)
- })
- }
- Worktree::Remote(worktree) => {
- let buffer_id = buffer.remote_id();
- let version = buffer.version();
- let project_id = worktree.project_id();
- let client = worktree.client();
- cx.spawn(async move |_| {
+ let content = match &version {
+ Some(version) => buffer.rope_for_version(version).clone(),
+ None => buffer.as_rope().clone(),
+ };
+ let version = version.unwrap_or(buffer.version());
+ let buffer_id = buffer.remote_id();
+
+ let rx = repo.read(cx).send_job(move |state, _| async move {
+ match state {
+ RepositoryState::Local { backend, .. } => backend
+ .blame(repo_path.clone(), content)
+ .await
+ .with_context(|| format!("Failed to blame {:?}", repo_path.0))
+ .map(Some),
+ RepositoryState::Remote { project_id, client } => {
let response = client
.request(proto::BlameBuffer {
- project_id,
+ project_id: project_id.to_proto(),
buffer_id: buffer_id.into(),
version: serialize_version(&version),
})
.await?;
Ok(deserialize_blame_buffer_response(response))
- })
+ }
}
- }
+ });
+
+ cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
}
pub fn get_permalink_to_line(
@@ -810,64 +858,53 @@ impl GitStore {
selection: Range<u32>,
cx: &App,
) -> Task<Result<url::Url>> {
- let buffer = buffer.read(cx);
- let Some(file) = File::from_dyn(buffer.file()) else {
+ let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
return Task::ready(Err(anyhow!("buffer has no file")));
};
- match file.worktree.read(cx) {
- Worktree::Local(worktree) => {
- let repository = self
- .repository_and_path_for_project_path(
- &(worktree.id(), file.path.clone()).into(),
- cx,
- )
- .map(|(repository, _)| repository);
- let Some((local_repo_entry, repo_entry)) = repository.and_then(|repository| {
- let repository = repository.read(cx);
- let repo_entry = repository.repository_entry.clone();
- Some((worktree.get_local_repo(&repo_entry)?, repo_entry))
- }) else {
- // If we're not in a Git repo, check whether this is a Rust source
- // file in the Cargo registry (presumably opened with go-to-definition
- // from a normal Rust file). If so, we can put together a permalink
- // using crate metadata.
- if buffer
- .language()
- .is_none_or(|lang| lang.name() != "Rust".into())
- {
- return Task::ready(Err(anyhow!("no permalink available")));
- }
- let Some(file_path) = worktree.absolutize(&file.path).ok() else {
- return Task::ready(Err(anyhow!("no permalink available")));
- };
- return cx.spawn(async move |cx| {
- let provider_registry =
- cx.update(GitHostingProviderRegistry::default_global)?;
- get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
- .map_err(|_| anyhow!("no permalink available"))
- });
- };
-
- let path = match local_repo_entry.relativize(&file.path) {
- Ok(RepoPath(path)) => path,
- Err(e) => return Task::ready(Err(e)),
- };
+ let Some((repo, repo_path)) = self.repository_and_path_for_project_path(
+ &(file.worktree.read(cx).id(), file.path.clone()).into(),
+ cx,
+ ) else {
+ // If we're not in a Git repo, check whether this is a Rust source
+ // file in the Cargo registry (presumably opened with go-to-definition
+ // from a normal Rust file). If so, we can put together a permalink
+ // using crate metadata.
+ if buffer
+ .read(cx)
+ .language()
+ .is_none_or(|lang| lang.name() != "Rust".into())
+ {
+ return Task::ready(Err(anyhow!("no permalink available")));
+ }
+ let Some(file_path) = file.worktree.read(cx).absolutize(&file.path).ok() else {
+ return Task::ready(Err(anyhow!("no permalink available")));
+ };
+ return cx.spawn(async move |cx| {
+ let provider_registry = cx.update(GitHostingProviderRegistry::default_global)?;
+ get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
+ .map_err(|_| anyhow!("no permalink available"))
+ });
- let remote = repo_entry
- .branch()
- .and_then(|b| b.upstream.as_ref())
- .and_then(|b| b.remote_name())
- .unwrap_or("origin")
- .to_string();
+ // TODO remote case
+ };
- let repo = local_repo_entry.repo().clone();
- cx.spawn(async move |cx| {
- let origin_url = repo
+ let buffer_id = buffer.read(cx).remote_id();
+ let branch = repo.read(cx).branch.clone();
+ let remote = branch
+ .as_ref()
+ .and_then(|b| b.upstream.as_ref())
+ .and_then(|b| b.remote_name())
+ .unwrap_or("origin")
+ .to_string();
+ let rx = repo.read(cx).send_job(move |state, cx| async move {
+ match state {
+ RepositoryState::Local { backend, .. } => {
+ let origin_url = backend
.remote_url(&remote)
.ok_or_else(|| anyhow!("remote \"{remote}\" not found"))?;
- let sha = repo
+ let sha = backend
.head_sha()
.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
@@ -878,7 +915,7 @@ impl GitStore {
parse_git_remote_url(provider_registry, &origin_url)
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
- let path = path
+ let path = repo_path
.to_str()
.ok_or_else(|| anyhow!("failed to convert path to string"))?;
@@ -890,16 +927,11 @@ impl GitStore {
selection: Some(selection),
},
))
- })
- }
- Worktree::Remote(worktree) => {
- let buffer_id = buffer.remote_id();
- let project_id = worktree.project_id();
- let client = worktree.client();
- cx.spawn(async move |_| {
+ }
+ RepositoryState::Remote { project_id, client } => {
let response = client
.request(proto::GetPermalinkToLine {
- project_id,
+ project_id: project_id.to_proto(),
buffer_id: buffer_id.into(),
selection: Some(proto::Range {
start: selection.start as u64,
@@ -909,20 +941,23 @@ impl GitStore {
.await?;
url::Url::parse(&response.permalink).context("failed to parse permalink")
- })
+ }
}
- }
+ });
+ cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
}
fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> {
match &self.state {
GitStoreState::Local {
- downstream_client, ..
+ downstream: downstream_client,
+ ..
} => downstream_client
.as_ref()
.map(|state| (state.client.clone(), state.project_id)),
GitStoreState::Ssh {
- downstream_client, ..
+ downstream: downstream_client,
+ ..
} => downstream_client.clone(),
GitStoreState::Remote { .. } => None,
}
@@ -940,160 +975,148 @@ impl GitStore {
}
}
- fn project_environment(&self) -> Option<Entity<ProjectEnvironment>> {
- match &self.state {
- GitStoreState::Local { environment, .. } => Some(environment.clone()),
- GitStoreState::Ssh { environment, .. } => Some(environment.clone()),
- GitStoreState::Remote { .. } => None,
- }
- }
-
- fn project_id(&self) -> Option<ProjectId> {
- match &self.state {
- GitStoreState::Local { .. } => None,
- GitStoreState::Ssh { .. } => Some(ProjectId(proto::SSH_PROJECT_ID)),
- GitStoreState::Remote { project_id, .. } => Some(*project_id),
- }
- }
-
fn on_worktree_store_event(
&mut self,
worktree_store: Entity<WorktreeStore>,
event: &WorktreeStoreEvent,
cx: &mut Context<Self>,
) {
+ let GitStoreState::Local {
+ project_environment,
+ downstream,
+ next_repository_id,
+ fs,
+ } = &self.state
+ else {
+ return;
+ };
+
match event {
- WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => {
- // We should only get this event for a local project.
- self.update_repositories(&worktree_store, cx);
- if self.is_local() {
- if let Some(worktree) =
- worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
- {
- self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
- }
+ WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => {
+ let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default();
+ for (relative_path, _, _) in updated_entries.iter() {
+ let Some((repo, repo_path)) = self.repository_and_path_for_project_path(
+ &(*worktree_id, relative_path.clone()).into(),
+ cx,
+ ) else {
+ continue;
+ };
+ paths_by_git_repo.entry(repo).or_default().push(repo_path)
+ }
+
+ for (repo, paths) in paths_by_git_repo {
+ repo.update(cx, |repo, cx| {
+ repo.paths_changed(
+ paths,
+ downstream
+ .as_ref()
+ .map(|downstream| downstream.updates_tx.clone()),
+ cx,
+ );
+ });
}
- cx.emit(GitEvent::GitStateUpdated);
}
- WorktreeStoreEvent::WorktreeAdded(_) => {}
- _ => {
- cx.emit(GitEvent::FileSystemUpdated);
+ WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => {
+ self.update_repositories_from_worktrees(
+ project_environment.clone(),
+ next_repository_id.clone(),
+ downstream
+ .as_ref()
+ .map(|downstream| downstream.updates_tx.clone()),
+ changed_repos.clone(),
+ fs.clone(),
+ cx,
+ );
+ if let Some(worktree) = worktree_store.read(cx).worktree_for_id(*worktree_id, cx) {
+ self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
+ }
}
+ _ => {}
}
}
- fn update_repositories(
+ fn on_repository_event(
&mut self,
- worktree_store: &Entity<WorktreeStore>,
- cx: &mut Context<GitStore>,
+ repo: Entity<Repository>,
+ event: &RepositoryEvent,
+ cx: &mut Context<Self>,
) {
- let mut new_repositories = HashMap::default();
- let git_store = cx.weak_entity();
- worktree_store.update(cx, |worktree_store, cx| {
- for worktree in worktree_store.worktrees() {
- worktree.update(cx, |worktree, cx| {
- let snapshot = worktree.snapshot();
- for repo_entry in snapshot.repositories().iter() {
- let git_repo_and_merge_message = worktree
- .as_local()
- .and_then(|local_worktree| local_worktree.get_local_repo(repo_entry))
- .map(|local_repo| {
- (
- RepositoryState::Local(local_repo.repo().clone()),
- local_repo.merge_message.clone(),
- )
- })
- .or_else(|| {
- let git_repo = RepositoryState::Remote {
- project_id: self.project_id()?,
- client: self
- .upstream_client()
- .context("no upstream client")
- .log_err()?
- .clone(),
- work_directory_id: repo_entry.work_directory_id(),
- };
- Some((git_repo, None))
- });
-
- let Some((git_repo, merge_message)) = git_repo_and_merge_message else {
- continue;
- };
-
- let existing_repo = self
- .repositories
- .values()
- .find(|repo| repo.read(cx).id() == repo_entry.work_directory_id());
-
- let repo = if let Some(existing_repo) = existing_repo {
- // Update the statuses and merge message but keep everything else.
- let existing_repo = existing_repo.clone();
- existing_repo.update(cx, |existing_repo, _| {
- existing_repo.repository_entry = repo_entry.clone();
- if matches!(git_repo, RepositoryState::Local { .. }) {
- existing_repo.merge_message = merge_message;
- existing_repo.completed_scan_id = worktree.completed_scan_id();
- }
- });
- existing_repo
- } else {
- cx.new(|_| Repository {
- worktree_id: Some(worktree.id()),
- project_environment: self
- .project_environment()
- .as_ref()
- .map(|env| env.downgrade()),
- git_store: git_store.clone(),
- askpass_delegates: Default::default(),
- latest_askpass_id: 0,
- repository_entry: repo_entry.clone(),
- job_sender: self.update_sender.clone(),
- merge_message,
- commit_message_buffer: None,
- completed_scan_id: worktree.completed_scan_id(),
- state: git_repo,
- })
- };
-
- // TODO only send out messages for repository snapshots that have changed
- let snapshot = repo.read(cx).repository_entry.clone();
- if let GitStoreState::Local {
- downstream_client: Some(state),
- ..
- } = &self.state
- {
- state
- .updates_tx
- .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
- .ok();
- }
- new_repositories.insert(repo_entry.work_directory_id(), repo);
- self.repositories.remove(&repo_entry.work_directory_id());
- }
- })
- }
- });
+ let id = repo.read(cx).id;
+ cx.emit(GitStoreEvent::RepositoryUpdated(
+ id,
+ event.clone(),
+ self.active_repo_id == Some(id),
+ ))
+ }
- if let GitStoreState::Local {
- downstream_client: Some(state),
- ..
- } = &self.state
- {
- for id in self.repositories.keys().cloned() {
- state
- .updates_tx
- .unbounded_send(DownstreamUpdate::RemoveRepository(id))
- .ok();
+ /// Update our list of repositories and schedule git scans in response to a notification from a worktree,
+ fn update_repositories_from_worktrees(
+ &mut self,
+ project_environment: Entity<ProjectEnvironment>,
+ next_repository_id: Arc<AtomicU64>,
+ updates_tx: Option<mpsc::UnboundedSender<DownstreamUpdate>>,
+ updated_git_repositories: UpdatedGitRepositoriesSet,
+ fs: Arc<dyn Fs>,
+ cx: &mut Context<Self>,
+ ) {
+ let mut removed_ids = Vec::new();
+ for update in updated_git_repositories.iter() {
+ if let Some((id, existing)) = self.repositories.iter().find(|(_, repo)| {
+ Some(&repo.read(cx).work_directory_abs_path)
+ == update.old_work_directory_abs_path.as_ref()
+ }) {
+ if let Some(new_work_directory_abs_path) =
+ update.new_work_directory_abs_path.clone()
+ {
+ existing.update(cx, |existing, cx| {
+ existing.snapshot.work_directory_abs_path = new_work_directory_abs_path;
+ existing.schedule_scan(updates_tx.clone(), cx);
+ });
+ } else {
+ removed_ids.push(*id);
+ }
+ } else if let Some((work_directory_abs_path, dot_git_abs_path)) = update
+ .new_work_directory_abs_path
+ .clone()
+ .zip(update.dot_git_abs_path.clone())
+ {
+ let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release));
+ let git_store = cx.weak_entity();
+ let repo = cx.new(|cx| {
+ let mut repo = Repository::local(
+ id,
+ work_directory_abs_path,
+ dot_git_abs_path,
+ project_environment.downgrade(),
+ fs.clone(),
+ git_store,
+ cx,
+ );
+ repo.schedule_scan(updates_tx.clone(), cx);
+ repo
+ });
+ self._subscriptions
+ .push(cx.subscribe(&repo, Self::on_repository_event));
+ self.repositories.insert(id, repo);
+ cx.emit(GitStoreEvent::RepositoryAdded(id));
+ self.active_repo_id.get_or_insert_with(|| {
+ cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id)));
+ id
+ });
}
}
- self.repositories = new_repositories;
- if let Some(id) = self.active_repo_id.as_ref() {
- if !self.repositories.contains_key(id) {
+ for id in removed_ids {
+ if self.active_repo_id == Some(id) {
self.active_repo_id = None;
+ cx.emit(GitStoreEvent::ActiveRepositoryChanged(None));
+ }
+ self.repositories.remove(&id);
+ if let Some(updates_tx) = updates_tx.as_ref() {
+ updates_tx
+ .unbounded_send(DownstreamUpdate::RemoveRepository(id))
+ .ok();
}
- } else if let Some(&first_id) = self.repositories.keys().next() {
- self.active_repo_id = Some(first_id);
}
}
@@ -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()
}
@@ -1,12 +1,19 @@
#![allow(clippy::format_collect)]
-use crate::{Event, task_inventory::TaskContexts, task_store::TaskSettingsLocation, *};
+use crate::{
+ Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
+ *,
+};
use buffer_diff::{
BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks,
};
use fs::FakeFs;
use futures::{StreamExt, future};
-use git::repository::RepoPath;
+use git::{
+ repository::RepoPath,
+ status::{StatusCode, TrackedStatus},
+};
+use git2::RepositoryInitOptions;
use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal};
use http_client::Url;
use language::{
@@ -21,6 +28,7 @@ use lsp::{
};
use parking_lot::Mutex;
use paths::tasks_file;
+use postage::stream::Stream as _;
use pretty_assertions::{assert_eq, assert_matches};
use serde_json::json;
#[cfg(not(windows))]
@@ -7067,7 +7075,7 @@ async fn test_repository_and_path_for_project_path(
(
path,
result.map(|(repo, repo_path)| {
- (Path::new(repo).to_owned(), RepoPath::from(repo_path))
+ (Path::new(repo).into(), RepoPath::from(repo_path))
}),
)
})
@@ -7079,13 +7087,7 @@ async fn test_repository_and_path_for_project_path(
let result = maybe!({
let (repo, repo_path) =
git_store.repository_and_path_for_project_path(&project_path, cx)?;
- Some((
- repo.read(cx)
- .repository_entry
- .work_directory_abs_path
- .clone(),
- repo_path,
- ))
+ Some((repo.read(cx).work_directory_abs_path.clone(), repo_path))
});
(path, result)
})
@@ -7160,13 +7162,830 @@ async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
.unwrap()
.0
.read(cx)
- .repository_entry
- .work_directory_abs_path,
+ .work_directory_abs_path
+ .as_ref(),
Path::new(path!("/root/home"))
);
});
}
+#[gpui::test]
+async fn test_git_repository_status(cx: &mut gpui::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 project.
+ 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 project = Project::test(
+ Arc::new(RealFs::new(None, cx.executor())),
+ [root.path()],
+ cx,
+ )
+ .await;
+
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project.repositories(cx).values().next().unwrap().clone()
+ });
+
+ // Check that the right git state is observed on startup
+ repository.read_with(cx, |repository, _| {
+ let entries = repository.cached_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();
+
+ repository.read_with(cx, |repository, _| {
+ let entries = repository.cached_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();
+
+ repository.read_with(cx, |repository, _cx| {
+ let entries = repository.cached_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 gpui::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 project = Project::test(
+ Arc::new(RealFs::new(None, cx.executor())),
+ [root.path()],
+ cx,
+ )
+ .await;
+
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project
+ .repositories(cx)
+ .values()
+ .find(|repo| repo.read(cx).work_directory_abs_path.ends_with("project"))
+ .unwrap()
+ .clone()
+ });
+
+ repository.read_with(cx, |repository, _cx| {
+ let entries = repository.cached_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 gpui::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 project = Project::test(
+ Arc::new(RealFs::new(None, cx.executor())),
+ [root.path().join(project_root).as_path()],
+ cx,
+ )
+ .await;
+
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project.repositories(cx).values().next().unwrap().clone()
+ });
+
+ // Ensure that the git status is loaded correctly
+ repository.read_with(cx, |repository, _cx| {
+ assert_eq!(
+ repository.work_directory_abs_path.canonicalize().unwrap(),
+ root.path().join("my-repo").canonicalize().unwrap()
+ );
+
+ assert_eq!(repository.status_for_path(&C_TXT.into()), None);
+ assert_eq!(
+ repository.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();
+
+ repository.read_with(cx, |repository, _cx| {
+ assert_eq!(repository.status_for_path(&C_TXT.into()), None);
+ assert_eq!(repository.status_for_path(&E_TXT.into()), None);
+ });
+}
+
+// TODO: this test fails on Windows because upon cherry-picking we don't get an event in the .git directory,
+// despite CHERRY_PICK_HEAD existing after the `git_cherry_pick` call and the conflicted path showing up in git status.
+#[cfg(not(windows))]
+#[gpui::test]
+async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+
+ let root = TempTree::new(json!({
+ "project": {
+ "a.txt": "a",
+ },
+ }));
+ let root_path = root.path();
+
+ let repo = git_init(&root_path.join("project"));
+ git_add("a.txt", &repo);
+ git_commit("init", &repo);
+
+ let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
+
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project.repositories(cx).values().next().unwrap().clone()
+ });
+
+ 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;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+ let conflicts = repository.update(cx, |repository, _| {
+ repository
+ .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 = repository.update(cx, |repository, _| {
+ repository
+ .merge_conflicts
+ .iter()
+ .cloned()
+ .collect::<Vec<_>>()
+ });
+ pretty_assertions::assert_eq!(conflicts, []);
+}
+
+#[gpui::test]
+async fn test_update_gitignore(cx: &mut gpui::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 project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project.repositories(cx).values().next().unwrap().clone()
+ });
+
+ // One file is unmodified, the other is ignored.
+ cx.read(|cx| {
+ assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, false);
+ assert_entry_git_state(tree.read(cx), repository.read(cx), "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| {
+ assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, true);
+ assert_entry_git_state(
+ tree.read(cx),
+ repository.read(cx),
+ "b.txt",
+ Some(StatusCode::Added),
+ false,
+ );
+ });
+}
+
+// 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 gpui::TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+ let root = TempTree::new(json!({
+ "projects": {
+ "project1": {
+ "a": "",
+ "b": "",
+ }
+ },
+
+ }));
+ let root_path = root.path();
+
+ 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();
+
+ let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
+
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project.repositories(cx).values().next().unwrap().clone()
+ });
+
+ repository.read_with(cx, |repository, _| {
+ assert_eq!(
+ repository.work_directory_abs_path.as_ref(),
+ root_path.join("projects/project1").as_path()
+ );
+ assert_eq!(
+ repository
+ .status_for_path(&"a".into())
+ .map(|entry| entry.status),
+ Some(StatusCode::Modified.worktree()),
+ );
+ assert_eq!(
+ repository
+ .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;
+
+ repository.read_with(cx, |repository, _| {
+ assert_eq!(
+ repository.work_directory_abs_path.as_ref(),
+ root_path.join("projects/project2").as_path()
+ );
+ assert_eq!(
+ repository.status_for_path(&"a".into()).unwrap().status,
+ StatusCode::Modified.worktree(),
+ );
+ assert_eq!(
+ repository.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 gpui::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
+ },
+
+ }));
+ let root_path = root.path();
+
+ 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 project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
+
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project.repositories(cx).values().next().unwrap().clone()
+ });
+
+ // Check that the right git state is observed on startup
+ repository.read_with(cx, |repository, _cx| {
+ assert_eq!(
+ repository.work_directory_abs_path.as_ref(),
+ root_path.join("project").as_path()
+ );
+
+ assert_eq!(
+ repository.status_for_path(&B_TXT.into()).unwrap().status,
+ FileStatus::Untracked,
+ );
+ assert_eq!(
+ repository.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.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ // The worktree detects that the file's git status has changed.
+ repository.read_with(cx, |repository, _| {
+ assert_eq!(
+ repository.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.
+ repository.read_with(cx, |repository, _cx| {
+ assert_eq!(
+ repository.status_for_path(&F_TXT.into()).unwrap().status,
+ FileStatus::Untracked,
+ );
+ assert_eq!(repository.status_for_path(&B_TXT.into()), None);
+ assert_eq!(repository.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
+ repository.read_with(cx, |repository, _cx| {
+ assert_eq!(repository.status_for_path(&A_TXT.into()), None);
+ assert_eq!(
+ repository.status_for_path(&B_TXT.into()).unwrap().status,
+ FileStatus::Untracked,
+ );
+ assert_eq!(
+ repository.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();
+
+ repository.read_with(cx, |repository, _cx| {
+ assert_eq!(
+ repository
+ .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();
+
+ repository.read_with(cx, |repository, _cx| {
+ assert_eq!(
+ repository
+ .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
+ .unwrap()
+ .status,
+ FileStatus::Untracked,
+ );
+ });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_rescan_with_gitignore(cx: &mut gpui::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 project = Project::test(fs.clone(), [path!("/root/tree").as_ref()], cx).await;
+
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ let repository = project.read_with(cx, |project, cx| {
+ project.repositories(cx).values().next().unwrap().clone()
+ });
+
+ tree.read_with(cx, |tree, _| {
+ tree.as_local()
+ .unwrap()
+ .manually_refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
+ })
+ .recv()
+ .await;
+
+ cx.read(|cx| {
+ assert_entry_git_state(
+ tree.read(cx),
+ repository.read(cx),
+ "tracked-dir/tracked-file1",
+ None,
+ false,
+ );
+ assert_entry_git_state(
+ tree.read(cx),
+ repository.read(cx),
+ "tracked-dir/ancestor-ignored-file1",
+ None,
+ false,
+ );
+ assert_entry_git_state(
+ tree.read(cx),
+ repository.read(cx),
+ "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| {
+ assert_entry_git_state(
+ tree.read(cx),
+ repository.read(cx),
+ "tracked-dir/tracked-file2",
+ Some(StatusCode::Added),
+ false,
+ );
+ assert_entry_git_state(
+ tree.read(cx),
+ repository.read(cx),
+ "tracked-dir/ancestor-ignored-file2",
+ None,
+ false,
+ );
+ assert_entry_git_state(
+ tree.read(cx),
+ repository.read(cx),
+ "ignored-dir/ignored-file2",
+ None,
+ true,
+ );
+ assert!(tree.read(cx).entry_for_path(".git").unwrap().is_ignored);
+ });
+}
+
async fn search(
project: &Entity<Project>,
query: SearchQuery,
@@ -7303,3 +8122,143 @@ fn get_all_tasks(
old.extend(new);
old
}
+
+#[track_caller]
+fn assert_entry_git_state(
+ tree: &Worktree,
+ repository: &Repository,
+ path: &str,
+ index_status: Option<StatusCode>,
+ is_ignored: bool,
+) {
+ assert_eq!(tree.abs_path(), repository.work_directory_abs_path);
+ let entry = tree
+ .entry_for_path(path)
+ .unwrap_or_else(|| panic!("entry {path} not found"));
+ let status = repository
+ .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}"
+ );
+}
+
+#[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");
+ }
+}
+
+#[cfg(not(windows))]
+#[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");
+}
+
+#[cfg(not(windows))]
+#[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");
+}
+
+#[cfg(not(windows))]
+#[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");
+}
+
+#[cfg(not(windows))]
+#[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()
+}
@@ -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
@@ -14,18 +14,13 @@ use futures::{
mpsc::{self, UnboundedSender},
oneshot,
},
- future::join_all,
select_biased,
task::Poll,
};
use fuzzy::CharBag;
use git::{
- COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, GitHostingProviderRegistry, INDEX_LOCK,
- LFS_DIR,
- repository::{Branch, GitRepository, RepoPath, UpstreamTrackingStatus},
- status::{
- FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
- },
+ COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR,
+ repository::RepoPath, status::GitSummary,
};
use gpui::{
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,
@@ -62,7 +57,7 @@ use std::{
pin::Pin,
sync::{
Arc,
- atomic::{self, AtomicI32, AtomicUsize, Ordering::SeqCst},
+ atomic::{AtomicUsize, Ordering::SeqCst},
},
time::{Duration, Instant},
};
@@ -159,7 +154,6 @@ pub struct Snapshot {
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
always_included_entries: Vec<Arc<Path>>,
- repositories: SumTree<RepositoryEntry>,
/// A number that increases every time the worktree begins scanning
/// a set of paths from the filesystem. This scanning could be caused
@@ -174,223 +168,6 @@ pub struct Snapshot {
completed_scan_id: usize,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct RepositoryEntry {
- /// The git status entries for this repository.
- /// Note that the paths on this repository are relative to the git work directory.
- /// If the .git folder is external to Zed, these paths will be relative to that folder,
- /// and this data structure might reference files external to this worktree.
- ///
- /// For example:
- ///
- /// my_root_folder/ <-- repository root
- /// .git
- /// my_sub_folder_1/
- /// project_root/ <-- Project root, Zed opened here
- /// changed_file_1 <-- File with changes, in worktree
- /// my_sub_folder_2/
- /// changed_file_2 <-- File with changes, out of worktree
- /// ...
- ///
- /// With this setup, this field would contain 2 entries, like so:
- /// - my_sub_folder_1/project_root/changed_file_1
- /// - my_sub_folder_2/changed_file_2
- pub statuses_by_path: SumTree<StatusEntry>,
- pub work_directory_id: ProjectEntryId,
- pub work_directory_abs_path: PathBuf,
- pub worktree_scan_id: usize,
- pub current_branch: Option<Branch>,
- pub current_merge_conflicts: TreeSet<RepoPath>,
-}
-
-impl RepositoryEntry {
- pub fn relativize_abs_path(&self, abs_path: &Path) -> Option<RepoPath> {
- Some(
- abs_path
- .strip_prefix(&self.work_directory_abs_path)
- .ok()?
- .into(),
- )
- }
-
- pub fn directory_contains_abs_path(&self, abs_path: impl AsRef<Path>) -> bool {
- abs_path.as_ref().starts_with(&self.work_directory_abs_path)
- }
-
- pub fn branch(&self) -> Option<&Branch> {
- self.current_branch.as_ref()
- }
-
- pub fn work_directory_id(&self) -> ProjectEntryId {
- self.work_directory_id
- }
-
- pub fn status(&self) -> impl Iterator<Item = StatusEntry> + '_ {
- self.statuses_by_path.iter().cloned()
- }
-
- pub fn status_len(&self) -> usize {
- self.statuses_by_path.summary().item_summary.count
- }
-
- pub fn status_summary(&self) -> GitSummary {
- self.statuses_by_path.summary().item_summary
- }
-
- pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
- self.statuses_by_path
- .get(&PathKey(path.0.clone()), &())
- .cloned()
- }
-
- pub fn initial_update(&self, project_id: u64) -> proto::UpdateRepository {
- proto::UpdateRepository {
- branch_summary: self.current_branch.as_ref().map(branch_to_proto),
- updated_statuses: self
- .statuses_by_path
- .iter()
- .map(|entry| entry.to_proto())
- .collect(),
- removed_statuses: Default::default(),
- current_merge_conflicts: self
- .current_merge_conflicts
- .iter()
- .map(|repo_path| repo_path.to_proto())
- .collect(),
- project_id,
- // This is semantically wrong---we want to move to having separate IDs for repositories.
- // But for the moment, RepositoryEntry isn't set up to provide that at this level, so we
- // shim it using the work directory's project entry ID. The pair of this + project ID will
- // be globally unique.
- id: self.work_directory_id().to_proto(),
- abs_path: self.work_directory_abs_path.as_path().to_proto(),
- entry_ids: vec![self.work_directory_id().to_proto()],
- // This is also semantically wrong, and should be replaced once we separate git repo updates
- // from worktree scans.
- scan_id: self.worktree_scan_id as u64,
- }
- }
-
- pub fn build_update(&self, old: &Self, project_id: u64) -> proto::UpdateRepository {
- let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
- let mut removed_statuses: Vec<String> = Vec::new();
-
- let mut new_statuses = self.statuses_by_path.iter().peekable();
- let mut old_statuses = old.statuses_by_path.iter().peekable();
-
- let mut current_new_entry = new_statuses.next();
- let mut current_old_entry = old_statuses.next();
- loop {
- match (current_new_entry, current_old_entry) {
- (Some(new_entry), Some(old_entry)) => {
- match new_entry.repo_path.cmp(&old_entry.repo_path) {
- Ordering::Less => {
- updated_statuses.push(new_entry.to_proto());
- current_new_entry = new_statuses.next();
- }
- Ordering::Equal => {
- if new_entry.status != old_entry.status {
- updated_statuses.push(new_entry.to_proto());
- }
- current_old_entry = old_statuses.next();
- current_new_entry = new_statuses.next();
- }
- Ordering::Greater => {
- removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
- current_old_entry = old_statuses.next();
- }
- }
- }
- (None, Some(old_entry)) => {
- removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
- current_old_entry = old_statuses.next();
- }
- (Some(new_entry), None) => {
- updated_statuses.push(new_entry.to_proto());
- current_new_entry = new_statuses.next();
- }
- (None, None) => break,
- }
- }
-
- proto::UpdateRepository {
- branch_summary: self.current_branch.as_ref().map(branch_to_proto),
- updated_statuses,
- removed_statuses,
- current_merge_conflicts: self
- .current_merge_conflicts
- .iter()
- .map(|path| path.as_ref().to_proto())
- .collect(),
- project_id,
- id: self.work_directory_id.to_proto(),
- abs_path: self.work_directory_abs_path.as_path().to_proto(),
- entry_ids: vec![self.work_directory_id.to_proto()],
- scan_id: self.worktree_scan_id as u64,
- }
- }
-}
-
-pub fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
- proto::Branch {
- is_head: branch.is_head,
- name: branch.name.to_string(),
- unix_timestamp: branch
- .most_recent_commit
- .as_ref()
- .map(|commit| commit.commit_timestamp as u64),
- upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream {
- ref_name: upstream.ref_name.to_string(),
- tracking: upstream
- .tracking
- .status()
- .map(|upstream| proto::UpstreamTracking {
- ahead: upstream.ahead as u64,
- behind: upstream.behind as u64,
- }),
- }),
- most_recent_commit: branch
- .most_recent_commit
- .as_ref()
- .map(|commit| proto::CommitSummary {
- sha: commit.sha.to_string(),
- subject: commit.subject.to_string(),
- commit_timestamp: commit.commit_timestamp,
- }),
- }
-}
-
-pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
- git::repository::Branch {
- is_head: proto.is_head,
- name: proto.name.clone().into(),
- upstream: proto
- .upstream
- .as_ref()
- .map(|upstream| git::repository::Upstream {
- ref_name: upstream.ref_name.to_string().into(),
- tracking: upstream
- .tracking
- .as_ref()
- .map(|tracking| {
- git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
- ahead: tracking.ahead as u32,
- behind: tracking.behind as u32,
- })
- })
- .unwrap_or(git::repository::UpstreamTracking::Gone),
- }),
- most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
- git::repository::CommitSummary {
- sha: commit.sha.to_string().into(),
- subject: commit.subject.to_string().into(),
- commit_timestamp: commit.commit_timestamp,
- has_parent: true,
- }
- }),
- }
-}
-
/// This path corresponds to the 'content path' of a repository in relation
/// to Zed's project root.
/// In the majority of the cases, this is the folder that contains the .git folder.
@@ -598,24 +375,20 @@ struct BackgroundScannerState {
removed_entries: HashMap<u64, Entry>,
changed_paths: Vec<Arc<Path>>,
prev_snapshot: Snapshot,
- git_hosting_provider_registry: Option<Arc<GitHostingProviderRegistry>>,
- repository_scans: HashMap<PathKey, Task<()>>,
}
#[derive(Debug, Clone)]
-pub struct LocalRepositoryEntry {
- pub(crate) work_directory_id: ProjectEntryId,
- pub(crate) work_directory: WorkDirectory,
- pub(crate) git_dir_scan_id: usize,
- pub(crate) status_scan_id: usize,
- pub(crate) repo_ptr: Arc<dyn GitRepository>,
+struct LocalRepositoryEntry {
+ work_directory_id: ProjectEntryId,
+ work_directory: WorkDirectory,
+ work_directory_abs_path: Arc<Path>,
+ git_dir_scan_id: usize,
+ original_dot_git_abs_path: Arc<Path>,
/// Absolute path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file
- pub(crate) dot_git_dir_abs_path: Arc<Path>,
+ dot_git_dir_abs_path: Arc<Path>,
/// Absolute path to the .git file, if we're in a git worktree.
- pub(crate) dot_git_worktree_abs_path: Option<Arc<Path>>,
- pub current_merge_head_shas: Vec<String>,
- pub merge_message: Option<String>,
+ dot_git_worktree_abs_path: Option<Arc<Path>>,
}
impl sum_tree::Item for LocalRepositoryEntry {
@@ -637,11 +410,11 @@ impl KeyedItem for LocalRepositoryEntry {
}
}
-impl LocalRepositoryEntry {
- pub fn repo(&self) -> &Arc<dyn GitRepository> {
- &self.repo_ptr
- }
-}
+//impl LocalRepositoryEntry {
+// pub fn repo(&self) -> &Arc<dyn GitRepository> {
+// &self.repo_ptr
+// }
+//}
impl Deref for LocalRepositoryEntry {
type Target = WorkDirectory;
@@ -1030,54 +803,6 @@ impl Worktree {
}
}
- pub fn load_staged_file(&self, path: &Path, cx: &App) -> Task<Result<Option<String>>> {
- match self {
- Worktree::Local(this) => {
- let path = Arc::from(path);
- let snapshot = this.snapshot();
- cx.spawn(async move |_cx| {
- if let Some(repo) = snapshot.local_repo_containing_path(&path) {
- if let Some(repo_path) = repo.relativize(&path).log_err() {
- if let Some(git_repo) =
- snapshot.git_repositories.get(&repo.work_directory_id)
- {
- return Ok(git_repo.repo_ptr.load_index_text(repo_path).await);
- }
- }
- }
- Err(anyhow!("No repository found for {path:?}"))
- })
- }
- Worktree::Remote(_) => {
- Task::ready(Err(anyhow!("remote worktrees can't yet load staged files")))
- }
- }
- }
-
- pub fn load_committed_file(&self, path: &Path, cx: &App) -> Task<Result<Option<String>>> {
- match self {
- Worktree::Local(this) => {
- let path = Arc::from(path);
- let snapshot = this.snapshot();
- cx.spawn(async move |_cx| {
- if let Some(repo) = snapshot.local_repo_containing_path(&path) {
- if let Some(repo_path) = repo.relativize(&path).log_err() {
- if let Some(git_repo) =
- snapshot.git_repositories.get(&repo.work_directory_id)
- {
- return Ok(git_repo.repo_ptr.load_committed_text(repo_path).await);
- }
- }
- }
- Err(anyhow!("No repository found for {path:?}"))
- })
- }
- Worktree::Remote(_) => Task::ready(Err(anyhow!(
- "remote worktrees can't yet load committed files"
- ))),
- }
- }
-
pub fn load_binary_file(
&self,
path: &Path,
@@ -1485,7 +1210,6 @@ impl LocalWorktree {
let share_private_files = self.share_private_files;
let next_entry_id = self.next_entry_id.clone();
let fs = self.fs.clone();
- let git_hosting_provider_registry = GitHostingProviderRegistry::try_global(cx);
let settings = self.settings.clone();
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
let background_scanner = cx.background_spawn({
@@ -1502,12 +1226,11 @@ impl LocalWorktree {
fs,
fs_case_sensitive,
status_updates_tx: scan_states_tx,
- scans_running: Arc::new(AtomicI32::new(0)),
executor: background,
scan_requests_rx,
path_prefixes_to_scan_rx,
next_entry_id,
- state: Arc::new(Mutex::new(BackgroundScannerState {
+ state: Mutex::new(BackgroundScannerState {
prev_snapshot: snapshot.snapshot.clone(),
snapshot,
scanned_dirs: Default::default(),
@@ -1515,9 +1238,7 @@ impl LocalWorktree {
paths_to_scan: Default::default(),
removed_entries: Default::default(),
changed_paths: Default::default(),
- repository_scans: HashMap::default(),
- git_hosting_provider_registry,
- })),
+ }),
phase: BackgroundScannerPhase::InitialScan,
share_private_files,
settings,
@@ -1561,11 +1282,11 @@ impl LocalWorktree {
fn set_snapshot(
&mut self,
- new_snapshot: LocalSnapshot,
+ mut new_snapshot: LocalSnapshot,
entry_changes: UpdatedEntriesSet,
cx: &mut Context<Worktree>,
) {
- let repo_changes = self.changed_repos(&self.snapshot, &new_snapshot);
+ let repo_changes = self.changed_repos(&self.snapshot, &mut new_snapshot);
self.snapshot = new_snapshot;
if let Some(share) = self.update_observer.as_mut() {
@@ -1586,81 +1307,78 @@ impl LocalWorktree {
fn changed_repos(
&self,
old_snapshot: &LocalSnapshot,
- new_snapshot: &LocalSnapshot,
+ new_snapshot: &mut LocalSnapshot,
) -> UpdatedGitRepositoriesSet {
let mut changes = Vec::new();
let mut old_repos = old_snapshot.git_repositories.iter().peekable();
- let mut new_repos = new_snapshot.git_repositories.iter().peekable();
+ let new_repos = new_snapshot.git_repositories.clone();
+ let mut new_repos = new_repos.iter().peekable();
loop {
match (new_repos.peek().map(clone), old_repos.peek().map(clone)) {
(Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => {
match Ord::cmp(&new_entry_id, &old_entry_id) {
Ordering::Less => {
- if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
- changes.push((
- entry.clone(),
- GitRepositoryChange {
- old_repository: None,
- },
- ));
- }
+ changes.push(UpdatedGitRepository {
+ work_directory_id: new_entry_id,
+ old_work_directory_abs_path: None,
+ new_work_directory_abs_path: Some(
+ new_repo.work_directory_abs_path.clone(),
+ ),
+ dot_git_abs_path: Some(new_repo.original_dot_git_abs_path.clone()),
+ });
new_repos.next();
}
Ordering::Equal => {
if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id
- || new_repo.status_scan_id != old_repo.status_scan_id
+ || new_repo.work_directory_abs_path
+ != old_repo.work_directory_abs_path
{
- if let Some(entry) = new_snapshot.entry_for_id(new_entry_id) {
- let old_repo =
- old_snapshot.repository_for_id(old_entry_id).cloned();
- changes.push((
- entry.clone(),
- GitRepositoryChange {
- old_repository: old_repo,
- },
- ));
- }
+ changes.push(UpdatedGitRepository {
+ work_directory_id: new_entry_id,
+ old_work_directory_abs_path: Some(
+ old_repo.work_directory_abs_path.clone(),
+ ),
+ new_work_directory_abs_path: Some(
+ new_repo.work_directory_abs_path.clone(),
+ ),
+ dot_git_abs_path: Some(
+ new_repo.original_dot_git_abs_path.clone(),
+ ),
+ });
}
new_repos.next();
old_repos.next();
}
Ordering::Greater => {
- if let Some(entry) = old_snapshot.entry_for_id(old_entry_id) {
- let old_repo =
- old_snapshot.repository_for_id(old_entry_id).cloned();
- changes.push((
- entry.clone(),
- GitRepositoryChange {
- old_repository: old_repo,
- },
- ));
- }
+ changes.push(UpdatedGitRepository {
+ work_directory_id: old_entry_id,
+ old_work_directory_abs_path: Some(
+ old_repo.work_directory_abs_path.clone(),
+ ),
+ new_work_directory_abs_path: None,
+ dot_git_abs_path: None,
+ });
old_repos.next();
}
}
}
- (Some((entry_id, _)), None) => {
- if let Some(entry) = new_snapshot.entry_for_id(entry_id) {
- changes.push((
- entry.clone(),
- GitRepositoryChange {
- old_repository: None,
- },
- ));
- }
+ (Some((entry_id, repo)), None) => {
+ changes.push(UpdatedGitRepository {
+ work_directory_id: entry_id,
+ old_work_directory_abs_path: None,
+ new_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
+ dot_git_abs_path: Some(repo.original_dot_git_abs_path.clone()),
+ });
new_repos.next();
}
- (None, Some((entry_id, _))) => {
- if let Some(entry) = old_snapshot.entry_for_id(entry_id) {
- let old_repo = old_snapshot.repository_for_id(entry_id).cloned();
- changes.push((
- entry.clone(),
- GitRepositoryChange {
- old_repository: old_repo,
- },
- ));
- }
+ (None, Some((entry_id, repo))) => {
+ changes.push(UpdatedGitRepository {
+ work_directory_id: entry_id,
+ old_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
+ new_work_directory_abs_path: None,
+ dot_git_abs_path: Some(repo.original_dot_git_abs_path.clone()),
+ });
old_repos.next();
}
(None, None) => break,
@@ -1696,10 +1414,6 @@ impl LocalWorktree {
self.settings.clone()
}
- pub fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> {
- self.git_repositories.get(&repo.work_directory_id)
- }
-
fn load_binary_file(
&self,
path: &Path,
@@ -2228,6 +1942,11 @@ impl LocalWorktree {
rx
}
+ #[cfg(feature = "test-support")]
+ pub fn manually_refresh_entries_for_paths(&self, paths: Vec<Arc<Path>>) -> barrier::Receiver {
+ self.refresh_entries_for_paths(paths)
+ }
+
pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<Path>) -> barrier::Receiver {
let (tx, rx) = barrier::channel();
self.path_prefixes_to_scan_tx
@@ -2527,7 +2246,6 @@ impl Snapshot {
always_included_entries: Default::default(),
entries_by_path: Default::default(),
entries_by_id: Default::default(),
- repositories: Default::default(),
scan_id: 1,
completed_scan_id: 0,
}
@@ -2646,26 +2364,6 @@ impl Snapshot {
Some(removed_entry.path)
}
- //#[cfg(any(test, feature = "test-support"))]
- //pub fn status_for_file(&self, path: impl AsRef<Path>) -> Option<FileStatus> {
- // let path = path.as_ref();
- // self.repository_for_path(path).and_then(|repo| {
- // let repo_path = repo.relativize(path).unwrap();
- // repo.statuses_by_path
- // .get(&PathKey(repo_path.0), &())
- // .map(|entry| entry.status)
- // })
- //}
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn status_for_file_abs_path(&self, abs_path: impl AsRef<Path>) -> Option<FileStatus> {
- let abs_path = abs_path.as_ref();
- let repo = self.repository_containing_abs_path(abs_path)?;
- let repo_path = repo.relativize_abs_path(abs_path)?;
- let status = repo.statuses_by_path.get(&PathKey(repo_path.0), &())?;
- Some(status.status)
- }
-
fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) {
self.abs_path = abs_path;
if root_name != self.root_name {
@@ -2674,7 +2372,7 @@ impl Snapshot {
}
}
- pub(crate) fn apply_remote_update(
+ fn apply_remote_update(
&mut self,
update: proto::UpdateWorktree,
always_included_paths: &PathMatcher,
@@ -2805,24 +2503,6 @@ impl Snapshot {
self.traverse_from_offset(true, true, include_ignored, start)
}
- pub fn repositories(&self) -> &SumTree<RepositoryEntry> {
- &self.repositories
- }
-
- /// Get the repository whose work directory contains the given path.
- fn repository_containing_abs_path(&self, abs_path: &Path) -> Option<&RepositoryEntry> {
- self.repositories
- .iter()
- .filter(|repo| repo.directory_contains_abs_path(abs_path))
- .last()
- }
-
- fn repository_for_id(&self, id: ProjectEntryId) -> Option<&RepositoryEntry> {
- self.repositories
- .iter()
- .find(|repo| repo.work_directory_id == id)
- }
-
pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
let empty_path = Path::new("");
self.entries_by_path
@@ -2905,20 +2585,13 @@ impl Snapshot {
}
impl LocalSnapshot {
- pub fn local_repo_for_work_directory_path(&self, path: &Path) -> Option<&LocalRepositoryEntry> {
+ fn local_repo_for_work_directory_path(&self, path: &Path) -> Option<&LocalRepositoryEntry> {
self.git_repositories
.iter()
.map(|(_, entry)| entry)
.find(|entry| entry.work_directory.path_key() == PathKey(path.into()))
}
- pub fn local_repo_containing_path(&self, path: &Path) -> Option<&LocalRepositoryEntry> {
- self.git_repositories
- .values()
- .filter(|local_repo| path.starts_with(&local_repo.path_key().0))
- .max_by_key(|local_repo| local_repo.path_key())
- }
-
fn build_update(
&self,
project_id: u64,
@@ -3046,7 +2719,7 @@ impl LocalSnapshot {
}
#[cfg(test)]
- pub(crate) fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
+ fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
self.entries_by_path
.cursor::<()>(&())
.filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored))
@@ -3125,26 +2798,6 @@ impl LocalSnapshot {
}
}
- #[cfg(test)]
- fn check_git_invariants(&self) {
- let dotgit_paths = self
- .git_repositories
- .iter()
- .map(|repo| repo.1.dot_git_dir_abs_path.clone())
- .collect::<HashSet<_>>();
- let work_dir_paths = self
- .repositories
- .iter()
- .map(|repo| repo.work_directory_abs_path.clone())
- .collect::<HashSet<_>>();
- assert_eq!(dotgit_paths.len(), work_dir_paths.len());
- assert_eq!(self.repositories.iter().count(), work_dir_paths.len());
- assert_eq!(self.git_repositories.iter().count(), work_dir_paths.len());
- for entry in self.repositories.iter() {
- self.git_repositories.get(&entry.work_directory_id).unwrap();
- }
- }
-
#[cfg(test)]
pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&Path, u64, bool)> {
let mut paths = Vec::new();
@@ -3288,7 +2941,7 @@ impl BackgroundScannerState {
}
fn remove_path(&mut self, path: &Path) {
- log::info!("background scanner removing path {path:?}");
+ log::debug!("background scanner removing path {path:?}");
let mut new_entries;
let removed_entries;
{
@@ -3343,11 +2996,6 @@ impl BackgroundScannerState {
self.snapshot
.git_repositories
.retain(|id, _| removed_ids.binary_search(id).is_err());
- self.snapshot.repositories.retain(&(), |repository| {
- removed_ids
- .binary_search(&repository.work_directory_id)
- .is_err()
- });
#[cfg(test)]
self.snapshot.check_invariants(false);
@@ -3358,7 +3006,7 @@ impl BackgroundScannerState {
dot_git_path: Arc<Path>,
fs: &dyn Fs,
watcher: &dyn Watcher,
- ) -> Option<LocalRepositoryEntry> {
+ ) {
let work_dir_path: Arc<Path> = match dot_git_path.parent() {
Some(parent_dir) => {
// Guard against repositories inside the repository metadata
@@ -3366,7 +3014,7 @@ impl BackgroundScannerState {
log::info!(
"not building git repository for nested `.git` directory, `.git` path in the worktree: {dot_git_path:?}"
);
- return None;
+ return;
};
log::info!(
"building git repository, `.git` path in the worktree: {dot_git_path:?}"
@@ -3380,7 +3028,7 @@ impl BackgroundScannerState {
log::info!(
"not building git repository for the worktree itself, `.git` path in the worktree: {dot_git_path:?}"
);
- return None;
+ return;
}
};
@@ -3391,7 +3039,7 @@ impl BackgroundScannerState {
dot_git_path,
fs,
watcher,
- )
+ );
}
fn insert_git_repository_for_path(
@@ -3401,7 +3049,6 @@ impl BackgroundScannerState {
fs: &dyn Fs,
watcher: &dyn Watcher,
) -> Option<LocalRepositoryEntry> {
- // TODO canonicalize here
log::info!("insert git repository for {dot_git_path:?}");
let work_dir_entry = self.snapshot.entry_for_path(work_directory.path_key().0)?;
let work_directory_abs_path = self
@@ -3421,6 +3068,7 @@ impl BackgroundScannerState {
let dot_git_abs_path = self.snapshot.abs_path.as_path().join(&dot_git_path);
+ // TODO add these watchers without building a whole repository by parsing .git-with-indirection
let t0 = Instant::now();
let repository = fs.open_repo(&dot_git_abs_path)?;
log::info!("opened git repo for {dot_git_abs_path:?}");
@@ -3443,41 +3091,21 @@ impl BackgroundScannerState {
// * `actual_dot_git_dir_abs_path` is the path to the actual .git directory. In git
// documentation this is called the "commondir".
watcher.add(&dot_git_abs_path).log_err()?;
- Some(Arc::from(dot_git_abs_path))
+ Some(Arc::from(dot_git_abs_path.as_path()))
};
log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
- if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() {
- git_hosting_providers::register_additional_providers(
- git_hosting_provider_registry,
- repository.clone(),
- );
- }
-
let work_directory_id = work_dir_entry.id;
- self.snapshot.repositories.insert_or_replace(
- RepositoryEntry {
- work_directory_id,
- work_directory_abs_path,
- current_branch: None,
- statuses_by_path: Default::default(),
- current_merge_conflicts: Default::default(),
- worktree_scan_id: 0,
- },
- &(),
- );
let local_repository = LocalRepositoryEntry {
work_directory_id,
work_directory,
git_dir_scan_id: 0,
- status_scan_id: 0,
- repo_ptr: repository.clone(),
+ original_dot_git_abs_path: dot_git_abs_path.as_path().into(),
dot_git_dir_abs_path: actual_dot_git_dir_abs_path.into(),
+ work_directory_abs_path: work_directory_abs_path.as_path().into(),
dot_git_worktree_abs_path,
- current_merge_head_shas: Default::default(),
- merge_message: None,
};
self.snapshot
@@ -3808,53 +3436,22 @@ pub enum PathChange {
Loaded,
}
-#[derive(Debug)]
-pub struct GitRepositoryChange {
- /// The previous state of the repository, if it already existed.
- pub old_repository: Option<RepositoryEntry>,
-}
-
-pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
-pub type UpdatedGitRepositoriesSet = Arc<[(Entry, GitRepositoryChange)]>;
-
#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct StatusEntry {
- pub repo_path: RepoPath,
- pub status: FileStatus,
-}
-
-impl StatusEntry {
- fn to_proto(&self) -> proto::StatusEntry {
- let simple_status = match self.status {
- FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32,
- FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32,
- FileStatus::Tracked(TrackedStatus {
- index_status,
- worktree_status,
- }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified {
- worktree_status
- } else {
- index_status
- }),
- };
-
- proto::StatusEntry {
- repo_path: self.repo_path.as_ref().to_proto(),
- simple_status,
- status: Some(status_to_proto(self.status)),
- }
- }
+pub struct UpdatedGitRepository {
+ /// ID of the repository's working directory.
+ ///
+ /// For a repo that's above the worktree root, this is the ID of the worktree root, and hence not unique.
+ /// It's included here to aid the GitStore in detecting when a repository's working directory is renamed.
+ pub work_directory_id: ProjectEntryId,
+ pub old_work_directory_abs_path: Option<Arc<Path>>,
+ pub new_work_directory_abs_path: Option<Arc<Path>>,
+ /// For a normal git repository checkout, the absolute path to the .git directory.
+ /// For a worktree, the absolute path to the worktree's subdirectory inside the .git directory.
+ pub dot_git_abs_path: Option<Arc<Path>>,
}
-impl TryFrom<proto::StatusEntry> for StatusEntry {
- type Error = anyhow::Error;
-
- fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
- let repo_path = RepoPath(Arc::<Path>::from_proto(value.repo_path));
- let status = status_from_proto(value.simple_status, value.status)?;
- Ok(Self { repo_path, status })
- }
-}
+pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
+pub type UpdatedGitRepositoriesSet = Arc<[UpdatedGitRepository]>;
#[derive(Clone, Debug)]
pub struct PathProgress<'a> {
@@ -3863,8 +3460,8 @@ pub struct PathProgress<'a> {
#[derive(Clone, Debug)]
pub struct PathSummary<S> {
- max_path: Arc<Path>,
- item_summary: S,
+ pub max_path: Arc<Path>,
+ pub item_summary: S,
}
impl<S: Summary> Summary for PathSummary<S> {
@@ -3899,75 +3496,6 @@ impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathProgress<'a
}
}
-#[derive(Clone, Debug)]
-pub struct AbsPathSummary {
- max_path: Arc<Path>,
-}
-
-impl Summary for AbsPathSummary {
- type Context = ();
-
- fn zero(_: &Self::Context) -> Self {
- Self {
- max_path: Path::new("").into(),
- }
- }
-
- fn add_summary(&mut self, rhs: &Self, _: &Self::Context) {
- self.max_path = rhs.max_path.clone();
- }
-}
-
-impl sum_tree::Item for RepositoryEntry {
- type Summary = AbsPathSummary;
-
- fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
- AbsPathSummary {
- max_path: self.work_directory_abs_path.as_path().into(),
- }
- }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct AbsPathKey(pub Arc<Path>);
-
-impl<'a> sum_tree::Dimension<'a, AbsPathSummary> for AbsPathKey {
- fn zero(_: &()) -> Self {
- Self(Path::new("").into())
- }
-
- fn add_summary(&mut self, summary: &'a AbsPathSummary, _: &()) {
- self.0 = summary.max_path.clone();
- }
-}
-
-impl sum_tree::KeyedItem for RepositoryEntry {
- type Key = AbsPathKey;
-
- fn key(&self) -> Self::Key {
- AbsPathKey(self.work_directory_abs_path.as_path().into())
- }
-}
-
-impl sum_tree::Item for StatusEntry {
- type Summary = PathSummary<GitSummary>;
-
- fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
- PathSummary {
- max_path: self.repo_path.0.clone(),
- item_summary: self.status.summary(),
- }
- }
-}
-
-impl sum_tree::KeyedItem for StatusEntry {
- type Key = PathKey;
-
- fn key(&self) -> Self::Key {
- PathKey(self.repo_path.0.clone())
- }
-}
-
impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
fn zero(_cx: &()) -> Self {
Default::default()
@@ -3978,6 +3506,14 @@ impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
}
}
+impl<'a> sum_tree::SeekTarget<'a, PathSummary<GitSummary>, (TraversalProgress<'a>, GitSummary)>
+ for PathTarget<'_>
+{
+ fn cmp(&self, cursor_location: &(TraversalProgress<'a>, GitSummary), _: &()) -> Ordering {
+ self.cmp_path(&cursor_location.0.max_path)
+ }
+}
+
impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathKey {
fn zero(_: &S::Context) -> Self {
Default::default()
@@ -4204,11 +3740,10 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
}
struct BackgroundScanner {
- state: Arc<Mutex<BackgroundScannerState>>,
+ state: Mutex<BackgroundScannerState>,
fs: Arc<dyn Fs>,
fs_case_sensitive: bool,
status_updates_tx: UnboundedSender<ScanState>,
- scans_running: Arc<AtomicI32>,
executor: BackgroundExecutor,
scan_requests_rx: channel::Receiver<ScanRequest>,
path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
@@ -4322,8 +3857,7 @@ impl BackgroundScanner {
state.snapshot.completed_scan_id = state.snapshot.scan_id;
}
- let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
- self.send_status_update(scanning, SmallVec::new());
+ self.send_status_update(false, SmallVec::new());
// Process any any FS events that occurred while performing the initial scan.
// For these events, update events cannot be as precise, because we didn't
@@ -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}"
- );
-}