use std::borrow::Cow;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use owo_colors::OwoColorize;
use prek_consts::PREK_TOML;
use tempfile::TempDir;
use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Value};
use crate::cli::run::Selectors;
use crate::cli::{ExitStatus, flag};
use crate::config;
use crate::git;
use crate::git::GIT_ROOT;
use crate::printer::Printer;
use crate::store::Store;
use crate::warn_user;
async fn get_head_rev(repo: &Path) -> Result<String> {
let head_rev = git::git_cmd("get head rev")?
.arg("rev-parse")
.arg("HEAD")
.current_dir(repo)
.output()
.await?
.stdout;
let head_rev = String::from_utf8_lossy(&head_rev).trim().to_string();
Ok(head_rev)
}
async fn clone_and_commit(repo_path: &Path, head_rev: &str, tmp_dir: &Path) -> Result<PathBuf> {
let shadow = tmp_dir.join("shadow-repo");
git::git_cmd("clone shadow repo")?
.arg("clone")
.arg(repo_path)
.arg(&shadow)
.output()
.await?;
git::git_cmd("checkout shadow repo")?
.arg("checkout")
.arg(head_rev)
.arg("-b")
.arg("_prek_tmp")
.current_dir(&shadow)
.output()
.await?;
let index_path = shadow.join(".git/index");
let objects_path = shadow.join(".git/objects");
let staged_files = git::get_staged_files(repo_path).await?;
if !staged_files.is_empty() {
git::git_cmd("add staged files to shadow")?
.arg("add")
.arg("--")
.args(&staged_files)
.current_dir(repo_path)
.env("GIT_INDEX_FILE", &index_path)
.env("GIT_OBJECT_DIRECTORY", &objects_path)
.output()
.await?;
}
let mut add_u_cmd = git::git_cmd("add unstaged to shadow")?;
add_u_cmd
.arg("add")
.arg("--update") .current_dir(repo_path)
.env("GIT_INDEX_FILE", &index_path)
.env("GIT_OBJECT_DIRECTORY", &objects_path)
.output()
.await?;
git::git_cmd("git commit")?
.arg("commit")
.arg("-m")
.arg("Temporary commit by prek try-repo")
.arg("--no-gpg-sign")
.arg("--no-edit")
.arg("--no-verify")
.current_dir(&shadow)
.env("GIT_AUTHOR_NAME", "prek test")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "prek test")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.output()
.await?;
Ok(shadow)
}
async fn prepare_repo_and_rev<'a>(
repo: &'a str,
rev: Option<&'a str>,
tmp_dir: &'a Path,
) -> Result<(Cow<'a, str>, String)> {
let repo_path = Path::new(repo);
let is_local = repo_path.is_dir();
if let Some(rev) = rev {
return Ok((Cow::Borrowed(repo), rev.to_string()));
}
let head_rev = if is_local {
get_head_rev(repo_path).await?
} else {
let head_rev = git::git_cmd("get head rev")?
.arg("ls-remote")
.arg("--exit-code")
.arg(repo)
.arg("HEAD")
.output()
.await?
.stdout;
String::from_utf8_lossy(&head_rev)
.split_ascii_whitespace()
.next()
.context("Failed to parse HEAD revision from git ls-remote output")?
.to_string()
};
if is_local && git::has_diff("HEAD", repo_path).await? {
warn_user!("Creating temporary repo with uncommitted changes...");
let shadow = clone_and_commit(repo_path, &head_rev, tmp_dir).await?;
let head_rev = get_head_rev(&shadow).await?;
Ok((Cow::Owned(shadow.to_string_lossy().to_string()), head_rev))
} else {
Ok((Cow::Borrowed(repo), head_rev))
}
}
fn render_repo_config_toml(repo_path: &str, rev: &str, hooks: Vec<String>) -> String {
let mut doc = DocumentMut::new();
let mut repo_table = toml_edit::Table::new();
repo_table["repo"] = toml_edit::value(repo_path);
repo_table["rev"] = toml_edit::value(rev);
let mut hooks_array = Array::new();
hooks_array.set_trailing_comma(true);
hooks_array.set_trailing("\n");
for hook_id in hooks {
let mut hook_table = InlineTable::new();
hook_table.insert("id", hook_id.into());
let mut value = Value::InlineTable(hook_table);
value.decor_mut().set_prefix("\n ");
hooks_array.push(value);
}
repo_table.insert("hooks", Item::Value(Value::Array(hooks_array)));
let mut repos = ArrayOfTables::new();
repos.push(repo_table);
doc["repos"] = Item::ArrayOfTables(repos);
doc.to_string()
}
pub(crate) async fn try_repo(
config: Option<PathBuf>,
repo: String,
rev: Option<String>,
run_args: crate::cli::RunArgs,
refresh: bool,
verbose: bool,
printer: Printer,
) -> Result<ExitStatus> {
if config.is_some() {
warn_user!("`--config` option is ignored when using `try-repo`");
}
let store = Store::from_settings()?;
let tmp_dir = TempDir::with_prefix_in("try-repo-", store.scratch_path())?;
let (repo_path, rev) = prepare_repo_and_rev(&repo, rev.as_deref(), tmp_dir.path())
.await
.context("Failed to determine repository and revision")?;
let store = Store::from_path(tmp_dir.path()).init()?;
let repo_config = config::RemoteRepo::new(repo_path.to_string(), rev.clone(), vec![]);
let repo_clone_path = store.clone_repo(&repo_config, None).await?;
let selectors = Selectors::load(&run_args.includes, &run_args.skips, GIT_ROOT.as_ref()?)?;
let manifest =
config::read_manifest(&repo_clone_path.join(prek_consts::PRE_COMMIT_HOOKS_YAML))?;
let hooks = manifest
.hooks
.into_iter()
.filter(|hook| selectors.matches_hook_id(&hook.id))
.map(|hook| hook.id)
.collect::<Vec<_>>();
let config_str = render_repo_config_toml(&repo_path, &rev, hooks);
let config_file = tmp_dir.path().join(PREK_TOML);
fs_err::tokio::write(&config_file, &config_str).await?;
writeln!(
printer.stdout(),
"{}",
format!("Using generated `{PREK_TOML}`:").cyan().bold()
)?;
writeln!(printer.stdout(), "{}", config_str.dimmed())?;
crate::cli::run(
&store,
Some(config_file),
vec![],
vec![],
run_args.stage,
run_args.from_ref,
run_args.to_ref,
run_args.all_files,
run_args.files,
run_args.directory,
run_args.last_commit,
run_args.show_diff_on_failure,
flag(run_args.fail_fast, run_args.no_fail_fast),
run_args.dry_run,
refresh,
run_args.extra,
verbose,
printer,
)
.await
}