mirror of
https://github.com/j178/prek.git
synced 2026-04-30 10:20:26 +02:00
413 lines
11 KiB
Rust
413 lines
11 KiB
Rust
use std::collections::HashSet;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Stdio;
|
|
use std::sync::LazyLock;
|
|
|
|
use anyhow::Result;
|
|
use itertools::Itertools;
|
|
use tokio::io::AsyncWriteExt;
|
|
use tracing::warn;
|
|
|
|
use crate::process;
|
|
use crate::process::Cmd;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum Error {
|
|
#[error(transparent)]
|
|
Command(#[from] process::Error),
|
|
#[error("Failed to find git: {0}")]
|
|
GitNotFound(#[from] which::Error),
|
|
#[error(transparent)]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
pub static GIT: LazyLock<Result<PathBuf, which::Error>> = LazyLock::new(|| which::which("git"));
|
|
|
|
static GIT_ENV_REMOVE: LazyLock<()> = LazyLock::new(|| {
|
|
let keep = &[
|
|
"GIT_EXEC_PATH",
|
|
"GIT_SSH",
|
|
"GIT_SSH_COMMAND",
|
|
"GIT_SSL_CAINFO",
|
|
"GIT_SSL_NO_VERIFY",
|
|
"GIT_CONFIG_COUNT",
|
|
"GIT_HTTP_PROXY_AUTHMETHOD",
|
|
"GIT_ALLOW_PROTOCOL",
|
|
"GIT_ASKPASS",
|
|
];
|
|
|
|
let to_remove = std::env::vars().filter(|(k, _)| {
|
|
k.starts_with("GIT_")
|
|
&& !k.starts_with("GIT_CONFIG_KEY_")
|
|
&& !k.starts_with("GIT_CONFIG_VALUE_")
|
|
&& !keep.contains(&k.as_str())
|
|
});
|
|
|
|
for (k, _) in to_remove {
|
|
unsafe { std::env::remove_var(&k) };
|
|
}
|
|
});
|
|
|
|
pub fn git_cmd(summary: &str) -> Result<Cmd, Error> {
|
|
let mut cmd = Cmd::new(GIT.as_ref().map_err(|&e| Error::GitNotFound(e))?, summary);
|
|
cmd.arg("-c").arg("core.useBuiltinFSMonitor=false");
|
|
|
|
LazyLock::force(&GIT_ENV_REMOVE);
|
|
|
|
Ok(cmd)
|
|
}
|
|
|
|
fn zsplit(s: &[u8]) -> Vec<String> {
|
|
s.split(|&b| b == b'\0')
|
|
.filter_map(|slice| {
|
|
if slice.is_empty() {
|
|
None
|
|
} else {
|
|
Some(String::from_utf8_lossy(slice).to_string())
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub async fn intent_to_add_files() -> Result<Vec<String>, Error> {
|
|
let output = git_cmd("get intent to add files")?
|
|
.arg("diff")
|
|
.arg("--no-ext-diff")
|
|
.arg("--ignore-submodules")
|
|
.arg("--diff-filter=A")
|
|
.arg("--name-only")
|
|
.arg("-z")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(zsplit(&output.stdout))
|
|
}
|
|
|
|
pub async fn get_changed_files(old: &str, new: &str) -> Result<Vec<String>, Error> {
|
|
let output = git_cmd("get changed files")?
|
|
.arg("diff")
|
|
.arg("--name-only")
|
|
.arg("--diff-filter=ACMRT")
|
|
.arg("--no-ext-diff") // Disable external diff drivers
|
|
.arg("-z") // Use NUL as line terminator
|
|
.arg(format!("{old}...{new}"))
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(zsplit(&output.stdout))
|
|
}
|
|
|
|
pub async fn get_all_files() -> Result<Vec<String>, Error> {
|
|
let output = git_cmd("get git all files")?
|
|
.arg("ls-files")
|
|
.arg("-z")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(zsplit(&output.stdout))
|
|
}
|
|
|
|
pub async fn get_git_dir() -> Result<PathBuf, Error> {
|
|
let output = git_cmd("get git dir")?
|
|
.arg("rev-parse")
|
|
.arg("--git-dir")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(PathBuf::from(
|
|
String::from_utf8_lossy(&output.stdout).trim(),
|
|
))
|
|
}
|
|
|
|
pub async fn get_git_common_dir() -> Result<PathBuf, Error> {
|
|
let output = git_cmd("get git common dir")?
|
|
.arg("rev-parse")
|
|
.arg("--git-common-dir")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
if output.stdout.trim_ascii().is_empty() {
|
|
Ok(get_git_dir().await?)
|
|
} else {
|
|
Ok(PathBuf::from(
|
|
String::from_utf8_lossy(&output.stdout).trim(),
|
|
))
|
|
}
|
|
}
|
|
|
|
pub async fn get_staged_files() -> Result<Vec<String>, Error> {
|
|
let output = git_cmd("get staged files")?
|
|
.arg("diff")
|
|
.arg("--staged")
|
|
.arg("--name-only")
|
|
.arg("--diff-filter=ACMRTUXB") // Everything except for D
|
|
.arg("--no-ext-diff") // Disable external diff drivers
|
|
.arg("-z") // Use NUL as line terminator
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(zsplit(&output.stdout))
|
|
}
|
|
|
|
pub async fn has_unmerged_paths() -> Result<bool, Error> {
|
|
let output = git_cmd("check has unmerged paths")?
|
|
.arg("ls-files")
|
|
.arg("--unmerged")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(!String::from_utf8_lossy(&output.stdout).trim().is_empty())
|
|
}
|
|
|
|
pub async fn is_in_merge_conflict() -> Result<bool, Error> {
|
|
let git_dir = get_git_dir().await?;
|
|
Ok(git_dir.join("MERGE_HEAD").try_exists()? && git_dir.join("MERGE_MSG").try_exists()?)
|
|
}
|
|
|
|
pub async fn get_conflicted_files() -> Result<Vec<String>, Error> {
|
|
let tree = git_cmd("git write-tree")?
|
|
.arg("write-tree")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
let output = git_cmd("get conflicted files")?
|
|
.arg("diff")
|
|
.arg("--name-only")
|
|
.arg("--no-ext-diff") // Disable external diff drivers
|
|
.arg("-z") // Use NUL as line terminator
|
|
.arg("-m")
|
|
.arg(String::from_utf8_lossy(&tree.stdout).trim())
|
|
.arg("HEAD")
|
|
.arg("MERGE_HEAD")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
Ok(zsplit(&output.stdout)
|
|
.into_iter()
|
|
.chain(parse_merge_msg_for_conflicts().await?)
|
|
.collect::<HashSet<String>>()
|
|
.into_iter()
|
|
.collect())
|
|
}
|
|
|
|
async fn parse_merge_msg_for_conflicts() -> Result<Vec<String>, Error> {
|
|
let git_dir = get_git_dir().await?;
|
|
let merge_msg = git_dir.join("MERGE_MSG");
|
|
let content = fs_err::read_to_string(&merge_msg)?;
|
|
let conflicts = content
|
|
.lines()
|
|
// Conflicted files start with tabs
|
|
.filter(|line| line.starts_with('\t') || line.starts_with("#\t"))
|
|
.map(|line| line.trim_start_matches('#').trim().to_string())
|
|
.collect();
|
|
Ok(conflicts)
|
|
}
|
|
|
|
pub async fn get_diff() -> Result<Vec<u8>, Error> {
|
|
let output = git_cmd("git diff")?
|
|
.arg("diff")
|
|
.arg("--no-ext-diff") // Disable external diff drivers
|
|
.arg("--no-textconv")
|
|
.arg("--ignore-submodules")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(output.stdout)
|
|
}
|
|
|
|
/// Create a tree object from the current index.
|
|
///
|
|
/// The name of the new tree object is printed to standard output.
|
|
/// The index must be in a fully merged state.
|
|
pub async fn write_tree() -> Result<String, Error> {
|
|
let output = git_cmd("git write-tree")?
|
|
.arg("write-tree")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
}
|
|
|
|
/// Get the path of the top-level directory of the working tree.
|
|
pub async fn get_root() -> Result<PathBuf, Error> {
|
|
let output = git_cmd("get git root")?
|
|
.arg("rev-parse")
|
|
.arg("--show-toplevel")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
Ok(PathBuf::from(
|
|
String::from_utf8_lossy(&output.stdout).trim(),
|
|
))
|
|
}
|
|
|
|
pub async fn is_dirty(path: &Path) -> Result<bool, Error> {
|
|
let mut cmd = git_cmd("check git is dirty")?;
|
|
let output = cmd
|
|
.arg("diff")
|
|
.arg("--quiet") // Implies `--exit-code`
|
|
.arg("--no-ext-diff") // Disable external diff drivers
|
|
.arg(path)
|
|
.check(false)
|
|
.output()
|
|
.await?;
|
|
if output.status.success() {
|
|
Ok(false)
|
|
} else if output.status.code() == Some(1) {
|
|
Ok(true)
|
|
} else {
|
|
Err(cmd.check_status(output.status).unwrap_err().into())
|
|
}
|
|
}
|
|
|
|
async fn init_repo(url: &str, path: &Path) -> Result<(), Error> {
|
|
git_cmd("init git repo")?
|
|
.arg("init")
|
|
.arg("--template=")
|
|
.arg(path)
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
git_cmd("add git remote")?
|
|
.current_dir(path)
|
|
.arg("remote")
|
|
.arg("add")
|
|
.arg("origin")
|
|
.arg(url)
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn shallow_clone(rev: &str, path: &Path) -> Result<(), Error> {
|
|
git_cmd("git shallow clone")?
|
|
.current_dir(path)
|
|
.arg("-c")
|
|
.arg("protocol.version=2")
|
|
.arg("fetch")
|
|
.arg("origin")
|
|
.arg(rev)
|
|
.arg("--depth=1")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
git_cmd("git checkout")?
|
|
.current_dir(path)
|
|
.arg("checkout")
|
|
.arg("FETCH_HEAD")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
git_cmd("update git submodules")?
|
|
.current_dir(path)
|
|
.arg("-c")
|
|
.arg("protocol.version=2")
|
|
.arg("submodule")
|
|
.arg("update")
|
|
.arg("--init")
|
|
.arg("--recursive")
|
|
.arg("--depth=1")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn full_clone(rev: &str, path: &Path) -> Result<(), Error> {
|
|
git_cmd("git full clone")?
|
|
.current_dir(path)
|
|
.arg("fetch")
|
|
.arg("origin")
|
|
.arg("--tags")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
git_cmd("git checkout")?
|
|
.current_dir(path)
|
|
.arg("checkout")
|
|
.arg(rev)
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
git_cmd("update git submodules")?
|
|
.current_dir(path)
|
|
.arg("submodule")
|
|
.arg("update")
|
|
.arg("--init")
|
|
.arg("--recursive")
|
|
.check(true)
|
|
.output()
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn clone_repo(url: &str, rev: &str, path: &Path) -> Result<(), Error> {
|
|
init_repo(url, path).await?;
|
|
|
|
if let Err(err) = shallow_clone(rev, path).await {
|
|
warn!(?err, "Failed to shallow clone, falling back to full clone");
|
|
full_clone(rev, path).await
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub async fn has_hooks_path_set() -> Result<bool> {
|
|
let output = git_cmd("get git hooks path")?
|
|
.arg("config")
|
|
.arg("--get")
|
|
.arg("core.hooksPath")
|
|
.check(false)
|
|
.output()
|
|
.await?;
|
|
if output.status.success() {
|
|
Ok(!String::from_utf8_lossy(&output.stdout).trim().is_empty())
|
|
} else {
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
pub async fn lfs_files<T: FromIterator<String>>(paths: &[&String]) -> Result<T, Error> {
|
|
let mut job = git_cmd("git check-attr")?
|
|
.arg("check-attr")
|
|
.arg("filter")
|
|
.arg("-z")
|
|
.arg("--stdin")
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.check(true)
|
|
// .output()
|
|
.spawn()?;
|
|
|
|
{
|
|
let mut stdin = job.stdin.take().expect("Failed to open stdin");
|
|
stdin.write_all(paths.iter().join("\0").as_ref()).await?;
|
|
}
|
|
|
|
Ok(
|
|
String::from_utf8_lossy(&job.wait_with_output().await?.stdout)
|
|
.trim()
|
|
.split('\0')
|
|
.tuples::<(_, _, _)>()
|
|
.filter_map(|(file, _, attr)| {
|
|
if attr == "lfs" {
|
|
Some(file.to_owned())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|