Finish removing git repository state and scanning logic from worktrees (#27568)

This PR completes the process of moving git repository state storage and
scanning logic from the worktree crate to `project::git_store`.

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
Cole Miller 2025-04-01 17:41:20 -04:00 committed by GitHub
parent 8f25251faf
commit e7290df02b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 3121 additions and 3529 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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);
})),

View file

@ -11,7 +11,7 @@ use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::future::Shared;
use futures::{FutureExt, StreamExt as _};
use git;
use git::repository::DiffType;
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
@ -19,7 +19,7 @@ use language_model::{
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason, TokenUsage,
};
use project::git_store::{GitStore, GitStoreCheckpoint};
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
use project::{Project, Worktree};
use prompt_store::{
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
@ -1446,48 +1446,61 @@ impl Thread {
(path, snapshot)
});
let Ok((worktree_path, snapshot)) = worktree_info else {
let Ok((worktree_path, _snapshot)) = worktree_info else {
return WorktreeSnapshot {
worktree_path: String::new(),
git_state: None,
};
};
let repo_info = git_store
let git_state = git_store
.update(cx, |git_store, cx| {
git_store
.repositories()
.values()
.find(|repo| repo.read(cx).worktree_id == Some(snapshot.id()))
.and_then(|repo| {
let repo = repo.read(cx);
Some((repo.branch().cloned(), repo.local_repository()?))
.find(|repo| {
repo.read(cx)
.abs_path_to_repo_path(&worktree.read(cx).abs_path())
.is_some()
})
.cloned()
})
.ok()
.flatten();
.flatten()
.map(|repo| {
repo.read_with(cx, |repo, _| {
let current_branch =
repo.branch.as_ref().map(|branch| branch.name.to_string());
repo.send_job(|state, _| async move {
let RepositoryState::Local { backend, .. } = state else {
return GitState {
remote_url: None,
head_sha: None,
current_branch,
diff: None,
};
};
// Extract git information
let git_state = match repo_info {
None => None,
Some((branch, repo)) => {
let current_branch = branch.map(|branch| branch.name.to_string());
let remote_url = repo.remote_url("origin");
let head_sha = repo.head_sha();
let remote_url = backend.remote_url("origin");
let head_sha = backend.head_sha();
let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
// Get diff asynchronously
let diff = repo
.diff(git::repository::DiffType::HeadToWorktree)
.await
.ok();
Some(GitState {
remote_url,
head_sha,
current_branch,
diff,
GitState {
remote_url,
head_sha,
current_branch,
diff,
}
})
})
}
});
let git_state = match git_state {
Some(git_state) => match git_state.ok() {
Some(git_state) => git_state.await.ok(),
None => None,
},
None => None,
};
WorktreeSnapshot {

View file

@ -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,
});
}

View file

@ -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,
});
}
}

View file

@ -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,
});
}
}

View file

@ -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()
})

View file

@ -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,

View file

@ -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()
})

View file

@ -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

View file

@ -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!()

View file

@ -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)

View file

@ -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)

View file

@ -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());

View file

@ -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();

View file

@ -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,

View file

@ -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;
};

View file

@ -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

View file

@ -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),

View file

@ -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) {

View file

@ -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();

View file

@ -1,11 +1,13 @@
use futures::{FutureExt, future::Shared};
use futures::{
FutureExt,
future::{Shared, WeakShared},
};
use std::{path::Path, sync::Arc};
use util::ResultExt;
use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
use settings::Settings as _;
use worktree::WorktreeId;
use crate::{
project_settings::{DirenvSettings, ProjectSettings},
@ -13,10 +15,9 @@ use crate::{
};
pub struct ProjectEnvironment {
worktree_store: Entity<WorktreeStore>,
cli_environment: Option<HashMap<String, String>>,
environments: HashMap<WorktreeId, Shared<Task<Option<HashMap<String, String>>>>>,
environment_error_messages: HashMap<WorktreeId, EnvironmentErrorMessage>,
environments: HashMap<Arc<Path>, WeakShared<Task<Option<HashMap<String, String>>>>>,
environment_error_messages: HashMap<Arc<Path>, EnvironmentErrorMessage>,
}
pub enum ProjectEnvironmentEvent {
@ -33,14 +34,15 @@ impl ProjectEnvironment {
) -> Entity<Self> {
cx.new(|cx| {
cx.subscribe(worktree_store, |this: &mut Self, _, event, _| {
if let WorktreeStoreEvent::WorktreeRemoved(_, id) = event {
this.remove_worktree_environment(*id);
if let WorktreeStoreEvent::WorktreeRemoved(_, _) = event {
this.environments.retain(|_, weak| weak.upgrade().is_some());
this.environment_error_messages
.retain(|abs_path, _| this.environments.contains_key(abs_path));
}
})
.detach();
Self {
worktree_store: worktree_store.clone(),
cli_environment,
environments: Default::default(),
environment_error_messages: Default::default(),
@ -48,11 +50,6 @@ impl ProjectEnvironment {
})
}
pub(crate) fn remove_worktree_environment(&mut self, worktree_id: WorktreeId) {
self.environment_error_messages.remove(&worktree_id);
self.environments.remove(&worktree_id);
}
/// Returns the inherited CLI environment, if this project was opened from the Zed CLI.
pub(crate) fn get_cli_environment(&self) -> Option<HashMap<String, String>> {
if let Some(mut env) = self.cli_environment.clone() {
@ -67,28 +64,22 @@ impl ProjectEnvironment {
/// environment errors associated with this project environment.
pub(crate) fn environment_errors(
&self,
) -> impl Iterator<Item = (&WorktreeId, &EnvironmentErrorMessage)> {
) -> impl Iterator<Item = (&Arc<Path>, &EnvironmentErrorMessage)> {
self.environment_error_messages.iter()
}
pub(crate) fn remove_environment_error(
&mut self,
worktree_id: WorktreeId,
cx: &mut Context<Self>,
) {
self.environment_error_messages.remove(&worktree_id);
pub(crate) fn remove_environment_error(&mut self, abs_path: &Path, cx: &mut Context<Self>) {
self.environment_error_messages.remove(abs_path);
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated);
}
/// Returns the project environment, if possible.
/// If the project was opened from the CLI, then the inherited CLI environment is returned.
/// If it wasn't opened from the CLI, and a worktree is given, then a shell is spawned in
/// the worktree's path, to get environment variables as if the user has `cd`'d into
/// the worktrees path.
/// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in
/// that directory, to get environment variables as if the user has `cd`'d there.
pub(crate) fn get_environment(
&mut self,
worktree_id: Option<WorktreeId>,
worktree_abs_path: Option<Arc<Path>>,
abs_path: Option<Arc<Path>>,
cx: &Context<Self>,
) -> Shared<Task<Option<HashMap<String, String>>>> {
if cfg!(any(test, feature = "test-support")) {
@ -111,74 +102,26 @@ impl ProjectEnvironment {
.shared();
}
let Some((worktree_id, worktree_abs_path)) = worktree_id.zip(worktree_abs_path) else {
let Some(abs_path) = abs_path else {
return Task::ready(None).shared();
};
if self
.worktree_store
.read(cx)
.worktree_for_id(worktree_id, cx)
.map(|w| !w.read(cx).is_local())
.unwrap_or(true)
if let Some(existing) = self
.environments
.get(&abs_path)
.and_then(|weak| weak.upgrade())
{
return Task::ready(None).shared();
}
if let Some(task) = self.environments.get(&worktree_id) {
task.clone()
existing
} else {
let task = self
.get_worktree_env(worktree_id, worktree_abs_path, cx)
.shared();
self.environments.insert(worktree_id, task.clone());
task
let env = get_directory_env(abs_path.clone(), cx).shared();
self.environments.insert(
abs_path.clone(),
env.downgrade()
.expect("environment task has not been polled yet"),
);
env
}
}
fn get_worktree_env(
&mut self,
worktree_id: WorktreeId,
worktree_abs_path: Arc<Path>,
cx: &Context<Self>,
) -> Task<Option<HashMap<String, String>>> {
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
cx.spawn(async move |this, cx| {
let (mut shell_env, error_message) = cx
.background_spawn({
let worktree_abs_path = worktree_abs_path.clone();
async move {
load_worktree_shell_environment(&worktree_abs_path, &load_direnv).await
}
})
.await;
if let Some(shell_env) = shell_env.as_mut() {
let path = shell_env
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables shell launched in {:?}. PATH={:?}",
worktree_abs_path,
path
);
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
}
if let Some(error) = error_message {
this.update(cx, |this, cx| {
this.environment_error_messages.insert(worktree_id, error);
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
})
.log_err();
}
shell_env
})
}
}
fn set_origin_marker(env: &mut HashMap<String, String>, origin: EnvironmentOrigin) {
@ -210,25 +153,25 @@ impl EnvironmentErrorMessage {
}
}
async fn load_worktree_shell_environment(
worktree_abs_path: &Path,
async fn load_directory_shell_environment(
abs_path: &Path,
load_direnv: &DirenvSettings,
) -> (
Option<HashMap<String, String>>,
Option<EnvironmentErrorMessage>,
) {
match smol::fs::metadata(worktree_abs_path).await {
match smol::fs::metadata(abs_path).await {
Ok(meta) => {
let dir = if meta.is_dir() {
worktree_abs_path
} else if let Some(parent) = worktree_abs_path.parent() {
abs_path
} else if let Some(parent) = abs_path.parent() {
parent
} else {
return (
None,
Some(EnvironmentErrorMessage(format!(
"Failed to load shell environment in {}: not a directory",
worktree_abs_path.display()
abs_path.display()
))),
);
};
@ -239,7 +182,7 @@ async fn load_worktree_shell_environment(
None,
Some(EnvironmentErrorMessage(format!(
"Failed to load shell environment in {}: {}",
worktree_abs_path.display(),
abs_path.display(),
err
))),
),
@ -387,3 +330,43 @@ async fn load_shell_environment(
(Some(parsed_env), direnv_error)
}
fn get_directory_env(
abs_path: Arc<Path>,
cx: &Context<ProjectEnvironment>,
) -> Task<Option<HashMap<String, String>>> {
let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone();
cx.spawn(async move |this, cx| {
let (mut shell_env, error_message) = cx
.background_spawn({
let abs_path = abs_path.clone();
async move { load_directory_shell_environment(&abs_path, &load_direnv).await }
})
.await;
if let Some(shell_env) = shell_env.as_mut() {
let path = shell_env
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables shell launched in {:?}. PATH={:?}",
abs_path,
path
);
set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell);
}
if let Some(error) = error_message {
this.update(cx, |this, cx| {
this.environment_error_messages.insert(abs_path, error);
cx.emit(ProjectEnvironmentEvent::ErrorsUpdated)
})
.log_err();
}
shell_env
})
}

File diff suppressed because it is too large Load diff

View file

@ -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)],
) {

View file

@ -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 {

View file

@ -24,7 +24,7 @@ mod direnv;
mod environment;
use buffer_diff::BufferDiff;
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
use git_store::{GitEvent, Repository};
use git_store::{Repository, RepositoryId};
pub mod search_history;
mod yarn;
@ -300,8 +300,6 @@ pub enum Event {
RevealInProjectPanel(ProjectEntryId),
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
GitStateUpdated,
ActiveRepositoryChanged,
}
pub enum DebugAdapterClientState {
@ -924,7 +922,6 @@ impl Project {
cx,
)
});
cx.subscribe(&git_store, Self::on_git_store_event).detach();
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@ -1064,13 +1061,7 @@ impl Project {
});
let git_store = cx.new(|cx| {
GitStore::ssh(
&worktree_store,
buffer_store.clone(),
environment.clone(),
ssh_proto.clone(),
cx,
)
GitStore::ssh(&worktree_store, buffer_store.clone(), ssh_proto.clone(), cx)
});
cx.subscribe(&ssh, Self::on_ssh_event).detach();
@ -1655,13 +1646,13 @@ impl Project {
pub fn shell_environment_errors<'a>(
&'a self,
cx: &'a App,
) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
self.environment.read(cx).environment_errors()
}
pub fn remove_environment_error(&mut self, worktree_id: WorktreeId, cx: &mut Context<Self>) {
pub fn remove_environment_error(&mut self, abs_path: &Path, cx: &mut Context<Self>) {
self.environment.update(cx, |environment, cx| {
environment.remove_environment_error(worktree_id, cx);
environment.remove_environment_error(abs_path, cx);
});
}
@ -2760,19 +2751,6 @@ impl Project {
}
}
fn on_git_store_event(
&mut self,
_: Entity<GitStore>,
event: &GitEvent,
cx: &mut Context<Self>,
) {
match event {
GitEvent::GitStateUpdated => cx.emit(Event::GitStateUpdated),
GitEvent::ActiveRepositoryChanged => cx.emit(Event::ActiveRepositoryChanged),
GitEvent::FileSystemUpdated | GitEvent::IndexWriteError(_) => {}
}
}
fn on_ssh_event(
&mut self,
_: Entity<SshRemoteClient>,
@ -4794,7 +4772,7 @@ impl Project {
self.git_store.read(cx).active_repository()
}
pub fn repositories<'a>(&self, cx: &'a App) -> &'a HashMap<ProjectEntryId, Entity<Repository>> {
pub fn repositories<'a>(&self, cx: &'a App) -> &'a HashMap<RepositoryId, Entity<Repository>> {
self.git_store.read(cx).repositories()
}

File diff suppressed because it is too large Load diff

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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 {

View file

@ -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)]

View file

@ -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()
})

View file

@ -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, ())));
}

View file

@ -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)

View file

@ -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();

View file

@ -30,7 +30,6 @@ fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
git.workspace = true
git_hosting_providers.workspace = true
gpui.workspace = true
ignore.workspace = true
language.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -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}"
);
}