git: Use the CLI for loading commit SHAs and details (#29351)
Since #28065 merged we've seen deadlocks inside iconv when opening Zed in a repository containing many submodules. These calls to iconv happen inside libgit2, in our implementations of the methods `head_sha`, `merge_head_shas`, and `show` on `RealGitRepository`. This PR moves those methods to use the git CLI instead, sidestepping the issue. For the sake of efficiency, a new `revparse_batch` method is added that uses `git cat-file` to resolve several ref names in one invocation. I originally intended to make `show` operate in batch mode as well (or instead), but I can't see a good way to do that with the git CLI; `git show` always bails on the first ref that it can't resolve, and `for-each-ref` doesn't support symbolic refs like `HEAD`. Separately, I removed the calls to `show` in `MergeDetails::load`, going back to only loading the SHAs of the various merge heads. Loading full commit details was intended to support the inlays feature that ended up being cut from #28065, and we can add it back in when we need it. Release Notes: - N/A
This commit is contained in:
parent
8cc2ade21c
commit
7f5c874a38
5 changed files with 129 additions and 117 deletions
|
@ -1,7 +1,7 @@
|
|||
use crate::commit::parse_git_diff_name_status;
|
||||
use crate::status::{GitStatus, StatusCode};
|
||||
use crate::{Oid, SHORT_SHA_LENGTH};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use collections::HashMap;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{AsyncWriteExt, FutureExt as _, select_biased};
|
||||
|
@ -13,17 +13,16 @@ use schemars::JsonSchema;
|
|||
use serde::Deserialize;
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::prelude::*;
|
||||
use std::path::Component;
|
||||
use std::process::{ExitStatus, Stdio};
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{
|
||||
future,
|
||||
io::{BufRead, BufReader, BufWriter, Read},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::MapSeekTarget;
|
||||
use thiserror::Error;
|
||||
|
@ -197,10 +196,20 @@ pub trait GitRepository: Send + Sync {
|
|||
/// Returns the URL of the remote with the given name.
|
||||
fn remote_url(&self, name: &str) -> Option<String>;
|
||||
|
||||
/// Returns the SHA of the current HEAD.
|
||||
fn head_sha(&self) -> Option<String>;
|
||||
/// Resolve a list of refs to SHAs.
|
||||
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;
|
||||
|
||||
fn merge_head_shas(&self) -> Vec<String>;
|
||||
fn head_sha(&self) -> BoxFuture<Option<String>> {
|
||||
async move {
|
||||
self.revparse_batch(vec!["HEAD".into()])
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.next()
|
||||
.flatten()
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn merge_message(&self) -> BoxFuture<Option<String>>;
|
||||
|
||||
|
@ -392,27 +401,37 @@ impl GitRepository for RealGitRepository {
|
|||
}
|
||||
|
||||
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
|
||||
let repo = self.repository.clone();
|
||||
let working_directory = self.working_directory();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let repo = repo.lock();
|
||||
let Ok(commit) = repo.revparse_single(&commit)?.into_commit() else {
|
||||
anyhow::bail!("{} is not a commit", commit);
|
||||
};
|
||||
let details = CommitDetails {
|
||||
sha: commit.id().to_string().into(),
|
||||
message: String::from_utf8_lossy(commit.message_raw_bytes())
|
||||
.to_string()
|
||||
.into(),
|
||||
commit_timestamp: commit.time().seconds(),
|
||||
author_email: String::from_utf8_lossy(commit.author().email_bytes())
|
||||
.to_string()
|
||||
.into(),
|
||||
author_name: String::from_utf8_lossy(commit.author().name_bytes())
|
||||
.to_string()
|
||||
.into(),
|
||||
};
|
||||
Ok(details)
|
||||
let working_directory = working_directory?;
|
||||
let output = new_std_command("git")
|
||||
.current_dir(&working_directory)
|
||||
.args([
|
||||
"--no-optional-locks",
|
||||
"show",
|
||||
"--no-patch",
|
||||
"--format=%H%x00%B%x00%at%x00%ae%x00%an",
|
||||
&commit,
|
||||
])
|
||||
.output()?;
|
||||
let output = std::str::from_utf8(&output.stdout)?;
|
||||
let fields = output.split('\0').collect::<Vec<_>>();
|
||||
if fields.len() != 5 {
|
||||
bail!("unexpected git-show output for {commit:?}: {output:?}")
|
||||
}
|
||||
let sha = fields[0].to_string().into();
|
||||
let message = fields[1].to_string().into();
|
||||
let commit_timestamp = fields[2].parse()?;
|
||||
let author_email = fields[3].to_string().into();
|
||||
let author_name = fields[4].to_string().into();
|
||||
Ok(CommitDetails {
|
||||
sha,
|
||||
message,
|
||||
commit_timestamp,
|
||||
author_email,
|
||||
author_name,
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
@ -702,34 +721,62 @@ impl GitRepository for RealGitRepository {
|
|||
remote.url().map(|url| url.to_string())
|
||||
}
|
||||
|
||||
fn head_sha(&self) -> Option<String> {
|
||||
Some(self.repository.lock().head().ok()?.target()?.to_string())
|
||||
}
|
||||
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>> {
|
||||
let working_directory = self.working_directory();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let working_directory = working_directory?;
|
||||
let mut process = new_std_command("git")
|
||||
.current_dir(&working_directory)
|
||||
.args([
|
||||
"--no-optional-locks",
|
||||
"cat-file",
|
||||
"--batch-check=%(objectname)",
|
||||
"-z",
|
||||
])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
fn merge_head_shas(&self) -> Vec<String> {
|
||||
let mut shas = Vec::default();
|
||||
self.repository
|
||||
.lock()
|
||||
.mergehead_foreach(|oid| {
|
||||
shas.push(oid.to_string());
|
||||
true
|
||||
let stdin = process
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no stdin for git cat-file subprocess"))?;
|
||||
let mut stdin = BufWriter::new(stdin);
|
||||
for rev in &revs {
|
||||
write!(&mut stdin, "{rev}\0")?;
|
||||
}
|
||||
drop(stdin);
|
||||
|
||||
let output = process.wait_with_output()?;
|
||||
let output = std::str::from_utf8(&output.stdout)?;
|
||||
let shas = output
|
||||
.lines()
|
||||
.map(|line| {
|
||||
if line.ends_with("missing") {
|
||||
None
|
||||
} else {
|
||||
Some(line.to_string())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if shas.len() != revs.len() {
|
||||
// In an octopus merge, git cat-file still only outputs the first sha from MERGE_HEAD.
|
||||
bail!("unexpected number of shas")
|
||||
}
|
||||
|
||||
Ok(shas)
|
||||
})
|
||||
.ok();
|
||||
if let Some(oid) = self
|
||||
.repository
|
||||
.lock()
|
||||
.find_reference("CHERRY_PICK_HEAD")
|
||||
.ok()
|
||||
.and_then(|reference| reference.target())
|
||||
{
|
||||
shas.push(oid.to_string())
|
||||
}
|
||||
shas
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn merge_message(&self) -> BoxFuture<Option<String>> {
|
||||
let path = self.path().join("MERGE_MSG");
|
||||
async move { std::fs::read_to_string(&path).ok() }.boxed()
|
||||
self.executor
|
||||
.spawn(async move { std::fs::read_to_string(&path).ok() })
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue