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 strum::IntoEnumIterator;
use tempfile::TempDir;
use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Value};
use crate::cli::run::Selectors;
use crate::cli::{ExitStatus, RunOptions, flag};
use crate::config::{self, Stage};
use crate::git;
use crate::git::GIT_ROOT;
use crate::hooks::{BuiltinHooks, MetaHooks};
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()?
.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()?
.arg("clone")
.arg(repo_path)
.arg(&shadow)
.output()
.await?;
git::git_cmd()?
.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::staged_files(repo_path).await?;
if !staged_files.is_empty() {
git::git_cmd()?
.arg("add")
.arg("--")
.file_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_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()?
.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)
}
struct PreparedRepo<'a> {
runtime_source: Cow<'a, str>,
display_source: Option<&'a str>,
rev: String,
}
async fn prepare_repo<'a>(
repo: &'a str,
rev: Option<&str>,
tmp_dir: &Path,
) -> Result<PreparedRepo<'a>> {
let repo_path = Path::new(repo);
let is_local = repo_path.is_dir();
let runtime_source = if is_local {
Cow::Owned(
dunce::canonicalize(repo_path)?
.to_string_lossy()
.into_owned(),
)
} else {
Cow::Borrowed(repo)
};
let repo_path = Path::new(runtime_source.as_ref());
if let Some(rev) = rev {
return Ok(PreparedRepo {
runtime_source,
display_source: is_local.then_some(repo),
rev: rev.to_string(),
});
}
let head_rev = if is_local {
get_head_rev(repo_path).await?
} else {
let head_rev = git::git_cmd()?
.arg("ls-remote")
.arg("--exit-code")
.arg(runtime_source.as_ref())
.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!("Local repository has uncommitted changes. Creating a temporary copy...");
let shadow = clone_and_commit(repo_path, &head_rev, tmp_dir).await?;
let head_rev = get_head_rev(&shadow).await?;
Ok(PreparedRepo {
runtime_source: Cow::Owned(shadow.to_string_lossy().into_owned()),
display_source: None,
rev: head_rev,
})
} else {
Ok(PreparedRepo {
runtime_source,
display_source: is_local.then_some(repo),
rev: head_rev,
})
}
}
fn render_repo_config_toml(repo_path: &str, rev: Option<&str>, hooks: &[String]) -> String {
let mut doc = DocumentMut::new();
let mut repo_table = toml_edit::Table::new();
repo_table["repo"] = toml_edit::value(repo_path);
if let Some(rev) = rev {
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()
}
fn select_predefined_hooks<H>(selectors: &Selectors) -> Vec<String>
where
H: AsRef<str> + IntoEnumIterator,
{
H::iter()
.filter(|hook| selectors.matches_hook_id(hook.as_ref()))
.map(|hook| hook.as_ref().to_string())
.collect()
}
pub(crate) async fn try_repo(
config: Option<PathBuf>,
repo: String,
rev: Option<String>,
run_args: RunOptions,
stage: Option<Stage>,
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 store = Store::from_path(tmp_dir.path())?.init()?;
let selectors = Selectors::load(&run_args.includes, &run_args.skips, GIT_ROOT.as_ref()?)?;
let predefined_hooks = match repo.as_str() {
"builtin" => Some(select_predefined_hooks::<BuiltinHooks>(&selectors)),
"meta" => Some(select_predefined_hooks::<MetaHooks>(&selectors)),
_ => None,
};
let (config_str, display_config_str) = if let Some(hooks) = predefined_hooks {
if rev.is_some() {
warn_user!("`--rev` option is ignored for the `{repo}` repo");
}
(render_repo_config_toml(&repo, None, &hooks), None)
} else {
let prepared = prepare_repo(&repo, rev.as_deref(), tmp_dir.path())
.await
.context("Failed to determine repository and revision")?;
let repo_config = config::RemoteRepo::new(
prepared.runtime_source.to_string(),
prepared.rev.clone(),
vec![],
);
let repo_clone_path = store.clone_repo(&repo_config, None).await?;
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(&prepared.runtime_source, Some(&prepared.rev), &hooks);
let display_config_str = prepared
.display_source
.map(|source| render_repo_config_toml(source, Some(&prepared.rev), &hooks));
(config_str, display_config_str)
};
let config_file = tmp_dir.path().join(PREK_TOML);
fs_err::tokio::write(&config_file, &config_str).await?;
let display_config_str = display_config_str.as_deref().unwrap_or(&config_str);
writeln!(
printer.stdout(),
"{}",
format!("Using generated `{PREK_TOML}`:").cyan().bold()
)?;
writeln!(printer.stdout(), "{}", display_config_str.dimmed())?;
let file_selection = run_args.file_selection.into();
crate::cli::run(
&store,
Some(config_file),
vec![],
vec![],
vec![],
vec![],
vec![],
stage,
file_selection,
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
}