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

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)