use anyhow::{Context, bail};
use camino::{Utf8Path, Utf8PathBuf};
use git_stub::{GitCommitHash, GitStub};
use git_stub_vcs::Vcs;
use std::process::Command;
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct VcsRevision(String);
NewtypeDebug! { () pub struct VcsRevision(String); }
NewtypeDeref! { () pub struct VcsRevision(String); }
NewtypeDisplay! { () pub struct VcsRevision(String); }
NewtypeFrom! { () pub struct VcsRevision(String); }
#[derive(Clone, Debug)]
pub(crate) struct RepoVcs {
kind: RepoVcsKind,
stub_vcs: Vcs,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RepoVcsKind {
Git,
Jj,
}
impl RepoVcs {
pub(crate) fn detect(repo_root: &Utf8Path) -> anyhow::Result<Self> {
let vcs = Vcs::detect(repo_root)
.with_context(|| format!("detecting VCS at {repo_root}"))?;
Self::from_git_stub_vcs(vcs)
}
#[allow(dead_code)]
pub(crate) fn git() -> anyhow::Result<Self> {
let vcs = Vcs::git().context("initializing Git VCS")?;
Self::from_git_stub_vcs(vcs)
}
#[allow(dead_code)]
pub(crate) fn jj() -> anyhow::Result<Self> {
let vcs = Vcs::jj().context("initializing Jujutsu VCS")?;
Self::from_git_stub_vcs(vcs)
}
fn from_git_stub_vcs(vcs: Vcs) -> anyhow::Result<Self> {
let kind = match vcs.name() {
git_stub_vcs::VcsName::Git => RepoVcsKind::Git,
git_stub_vcs::VcsName::Jj => RepoVcsKind::Jj,
other => bail!("unsupported VCS backend: {other:?}"),
};
Ok(Self { kind, stub_vcs: vcs })
}
pub(crate) fn kind(&self) -> RepoVcsKind {
self.kind
}
pub(crate) fn merge_base_head(
&self,
repo_root: &Utf8Path,
revision: &VcsRevision,
) -> anyhow::Result<GitCommitHash> {
match &self.kind {
RepoVcsKind::Git => {
super::git::git_merge_base_head(repo_root, revision)
}
RepoVcsKind::Jj => {
super::jj::jj_merge_base_head(repo_root, revision)
}
}
}
pub(crate) fn is_ancestor(
&self,
repo_root: &Utf8Path,
potential_ancestor: GitCommitHash,
commit: GitCommitHash,
) -> anyhow::Result<bool> {
match &self.kind {
RepoVcsKind::Git => super::git::git_is_ancestor(
repo_root,
potential_ancestor,
commit,
),
RepoVcsKind::Jj => {
super::jj::jj_is_ancestor(repo_root, potential_ancestor, commit)
}
}
}
pub(crate) fn list_files(
&self,
repo_root: &Utf8Path,
revision: GitCommitHash,
directory: &Utf8Path,
) -> anyhow::Result<Vec<Utf8PathBuf>> {
match &self.kind {
RepoVcsKind::Git => {
super::git::git_ls_tree(repo_root, revision, directory)
}
RepoVcsKind::Jj => {
super::jj::jj_list_files(repo_root, revision, directory)
}
}
}
pub(crate) fn show_file(
&self,
repo_root: &Utf8Path,
revision: GitCommitHash,
path: &Utf8Path,
) -> anyhow::Result<Vec<u8>> {
match &self.kind {
RepoVcsKind::Git => {
super::git::git_show_file(repo_root, revision, path)
}
RepoVcsKind::Jj => {
super::jj::jj_show_file(repo_root, revision, path)
}
}
}
pub(crate) fn first_commit_for_file(
&self,
repo_root: &Utf8Path,
revision: GitCommitHash,
path: &Utf8Path,
) -> anyhow::Result<GitCommitHash> {
match &self.kind {
RepoVcsKind::Git => {
super::git::git_first_commit_for_file(repo_root, revision, path)
}
RepoVcsKind::Jj => {
super::jj::jj_first_commit_for_file(repo_root, revision, path)
}
}
}
pub(crate) fn is_shallow_clone(&self, repo_root: &Utf8Path) -> bool {
match self.stub_vcs.is_shallow_clone(repo_root) {
Ok(is_shallow) => is_shallow,
Err(err) => {
eprintln!(
"warning: failed to check if repository is a \
shallow clone: {err:#}"
);
false
}
}
}
pub(crate) fn resolve_stub_contents(
&self,
git_stub: &GitStub,
repo_root: &Utf8Path,
) -> anyhow::Result<Vec<u8>> {
Ok(self.stub_vcs.read_git_stub_contents(git_stub, repo_root)?)
}
}
pub(super) fn do_run_bytes(cmd: &mut Command) -> anyhow::Result<Vec<u8>> {
let label = cmd_label(cmd);
let output = cmd.output().with_context(|| format!("invoking {:?}", cmd))?;
if output.status.success() {
return Ok(output.stdout);
}
bail!(
"command failed: {}: {}\n\
stderr:\n\
-----\n\
{}\n\
-----\n",
label,
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
pub(super) fn do_run(cmd: &mut Command) -> anyhow::Result<String> {
let label = cmd_label(cmd);
let stdout = do_run_bytes(cmd)?;
String::from_utf8(stdout).with_context(|| {
format!("command succeeded, but output was not UTF-8: {label}")
})
}
pub(super) fn cmd_label(cmd: &Command) -> String {
format!(
"{:?} {}",
cmd.get_program(),
cmd.get_args()
.map(|a| format!("{:?}", a))
.collect::<Vec<_>>()
.join(" ")
)
}