use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::git::{BranchInfo, ChangeEntry, CommitInfo, CommitMeta, DiffLine, FileStatus, GraphRow};
#[cfg(feature = "git")]
pub mod jj;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Caps {
pub write: bool,
}
pub trait Vcs: Send + Sync {
fn caps(&self) -> Caps;
fn workdir(&self, root: &Path) -> Option<PathBuf>;
fn statuses(&self, root: &Path) -> HashMap<PathBuf, FileStatus>;
fn ignored(&self, root: &Path) -> HashSet<PathBuf>;
fn branch(&self, root: &Path) -> Option<String>;
fn worktree_origin(&self, root: &Path) -> Option<String>;
fn file_diff(&self, root: &Path, file: &Path) -> Vec<DiffLine>;
fn changed_files(&self, root: &Path) -> Vec<ChangeEntry>;
fn log(&self, root: &Path, max: usize) -> Vec<CommitInfo>;
fn graph(
&self,
all: bool,
root: &Path,
base: Option<&str>,
lang: crate::i18n::Lang,
refs: Option<&[String]>,
) -> Vec<GraphRow>;
fn refs(&self, root: &Path) -> Vec<BranchInfo>;
fn ref_tip(&self, root: &Path, name: &str) -> Option<String>;
fn commit_meta(&self, root: &Path, id: &str) -> Option<CommitMeta>;
fn commit_diff(&self, root: &Path, id: &str) -> Vec<DiffLine>;
}
pub struct Git;
impl Vcs for Git {
fn caps(&self) -> Caps {
Caps { write: true }
}
fn workdir(&self, root: &Path) -> Option<PathBuf> {
crate::git::workdir(root)
}
fn statuses(&self, root: &Path) -> HashMap<PathBuf, FileStatus> {
crate::git::statuses(root)
}
fn ignored(&self, root: &Path) -> HashSet<PathBuf> {
crate::git::ignored(root)
}
fn branch(&self, root: &Path) -> Option<String> {
crate::git::branch(root)
}
fn worktree_origin(&self, root: &Path) -> Option<String> {
crate::git::worktree_origin(root)
}
fn file_diff(&self, root: &Path, file: &Path) -> Vec<DiffLine> {
crate::git::file_diff(root, file)
}
fn changed_files(&self, root: &Path) -> Vec<ChangeEntry> {
crate::git::changed_files(root)
}
fn log(&self, root: &Path, max: usize) -> Vec<CommitInfo> {
crate::git::log(root, max)
}
fn graph(
&self,
_all: bool,
root: &Path,
base: Option<&str>,
lang: crate::i18n::Lang,
refs: Option<&[String]>,
) -> Vec<GraphRow> {
crate::git::graph_with_base(root, base, lang, refs)
}
fn refs(&self, root: &Path) -> Vec<BranchInfo> {
crate::git::branches(root)
}
fn ref_tip(&self, root: &Path, name: &str) -> Option<String> {
crate::git::branch_tip(root, name)
}
fn commit_meta(&self, root: &Path, id: &str) -> Option<CommitMeta> {
crate::git::commit_meta(root, id)
}
fn commit_diff(&self, root: &Path, id: &str) -> Vec<DiffLine> {
crate::git::commit_diff(root, id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Preference {
Auto,
Git,
Jj,
}
impl Preference {
pub fn parse(s: &str) -> Self {
match s.trim().to_ascii_lowercase().as_str() {
"git" => Self::Git,
"jj" => Self::Jj,
_ => Self::Auto,
}
}
}
static PREFERENCE: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
pub fn set_preference(p: Preference) {
let v = match p {
Preference::Auto => 0,
Preference::Git => 1,
Preference::Jj => 2,
};
PREFERENCE.store(v, std::sync::atomic::Ordering::Relaxed);
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
fn preference() -> Preference {
match PREFERENCE.load(std::sync::atomic::Ordering::Relaxed) {
1 => Preference::Git,
2 => Preference::Jj,
_ => Preference::Auto,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VcsKind {
Git,
#[cfg(feature = "git")]
Jj,
}
#[cfg(feature = "git")]
pub struct Jj;
#[cfg(feature = "git")]
impl Vcs for Jj {
fn caps(&self) -> Caps {
Caps { write: false }
}
fn workdir(&self, root: &Path) -> Option<PathBuf> {
jj::workspace_root(root)
}
fn statuses(&self, root: &Path) -> HashMap<PathBuf, FileStatus> {
jj::statuses(root)
}
fn ignored(&self, root: &Path) -> HashSet<PathBuf> {
jj::ignored(root)
}
fn branch(&self, root: &Path) -> Option<String> {
jj::branch(root)
}
fn worktree_origin(&self, _root: &Path) -> Option<String> {
None
}
fn file_diff(&self, root: &Path, file: &Path) -> Vec<DiffLine> {
jj::file_diff(root, file)
}
fn changed_files(&self, root: &Path) -> Vec<ChangeEntry> {
jj::changed_files(root)
}
fn log(&self, root: &Path, max: usize) -> Vec<CommitInfo> {
jj::log(root, max)
}
fn graph(
&self,
all: bool,
root: &Path,
_base: Option<&str>,
_lang: crate::i18n::Lang,
_refs: Option<&[String]>,
) -> Vec<GraphRow> {
jj::graph(root, all.then_some("all()"), 400)
}
fn refs(&self, root: &Path) -> Vec<BranchInfo> {
jj::bookmarks(root)
}
fn ref_tip(&self, root: &Path, name: &str) -> Option<String> {
jj::bookmark_tip(root, name)
}
fn commit_meta(&self, root: &Path, id: &str) -> Option<CommitMeta> {
jj::commit_meta(root, id)
}
fn commit_diff(&self, root: &Path, id: &str) -> Vec<DiffLine> {
jj::commit_diff(root, id)
}
}
pub fn detect(root: &Path) -> VcsKind {
#[cfg(feature = "git")]
{
let pref = preference();
if pref == Preference::Git {
return VcsKind::Git;
}
let git_answers = pref == Preference::Auto && crate::git::workdir(root).is_some();
if !git_answers && jj::workspace_root(root).is_some() && jj::available() {
return VcsKind::Jj;
}
}
let _ = root;
VcsKind::Git
}
pub fn backend_for(root: &Path) -> &'static dyn Vcs {
match detect(root) {
VcsKind::Git => &Git,
#[cfg(feature = "git")]
VcsKind::Jj => &Jj,
}
}
pub fn workdir(root: &Path) -> Option<PathBuf> {
backend_for(root).workdir(root)
}
pub fn statuses(root: &Path) -> HashMap<PathBuf, FileStatus> {
backend_for(root).statuses(root)
}
pub fn ignored(root: &Path) -> HashSet<PathBuf> {
backend_for(root).ignored(root)
}
pub fn branch(root: &Path) -> Option<String> {
backend_for(root).branch(root)
}
pub fn worktree_origin(root: &Path) -> Option<String> {
backend_for(root).worktree_origin(root)
}
pub fn file_diff(root: &Path, file: &Path) -> Vec<DiffLine> {
backend_for(root).file_diff(root, file)
}
pub fn log(root: &Path, max: usize) -> Vec<CommitInfo> {
backend_for(root).log(root, max)
}
pub fn graph(
all: bool,
root: &Path,
base: Option<&str>,
lang: crate::i18n::Lang,
refs: Option<&[String]>,
) -> Vec<GraphRow> {
backend_for(root).graph(all, root, base, lang, refs)
}
pub fn refs(root: &Path) -> Vec<BranchInfo> {
backend_for(root).refs(root)
}
pub fn ref_tip(root: &Path, name: &str) -> Option<String> {
backend_for(root).ref_tip(root, name)
}
pub fn commit_meta(root: &Path, id: &str) -> Option<CommitMeta> {
backend_for(root).commit_meta(root, id)
}
pub fn commit_diff(root: &Path, id: &str) -> Vec<DiffLine> {
backend_for(root).commit_diff(root, id)
}
pub fn caps(root: &Path) -> Caps {
backend_for(root).caps()
}
pub fn changed_files(root: &Path) -> Vec<ChangeEntry> {
backend_for(root).changed_files(root)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backend_agrees_with_the_free_functions() {
for root in [
std::path::Path::new(env!("CARGO_MANIFEST_DIR")),
std::path::Path::new("/"),
] {
let b = backend_for(root);
assert_eq!(
b.workdir(root),
crate::git::workdir(root),
"workdir {root:?}"
);
assert_eq!(
b.statuses(root).len(),
crate::git::statuses(root).len(),
"statuses {root:?}"
);
assert_eq!(b.branch(root), crate::git::branch(root), "branch {root:?}");
assert_eq!(
b.worktree_origin(root),
crate::git::worktree_origin(root),
"worktree_origin {root:?}"
);
}
}
#[test]
fn backend_can_cross_a_thread() {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let handle = std::thread::spawn(move || backend_for(&root).workdir(&root));
assert!(handle.join().is_ok());
}
}