pub mod git;
pub mod jj;
pub(crate) mod shared;
pub mod types;
pub use types::{ComparisonContext, DiffBase, RefreshResult, StackPosition, UpstreamDivergence, VcsBackend, VcsEventType, VcsWatchPaths};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, OnceLock};
use anyhow::Result;
use rayon::ThreadPoolBuilder;
use crate::diff::FileDiff;
pub(crate) const PARALLEL_THRESHOLD: usize = 4;
const MAX_VCS_THREADS: usize = 16;
static VCS_POOL: OnceLock<rayon::ThreadPool> = OnceLock::new();
pub(crate) fn vcs_thread_pool() -> &'static rayon::ThreadPool {
VCS_POOL.get_or_init(|| {
let num_threads = std::thread::available_parallelism()
.map(|n| n.get().min(MAX_VCS_THREADS))
.unwrap_or(4);
ThreadPoolBuilder::new()
.num_threads(num_threads)
.build()
.expect("failed to build VCS thread pool")
})
}
pub fn detect_repo_dir(path: &Path) -> Option<(&'static str, PathBuf)> {
if path.join(".jj").is_dir() {
return Some(("jj", path.to_path_buf()));
}
if let Some(ancestor) = path.ancestors().find(|p| p.join(".jj").is_dir()) {
return Some(("jj", ancestor.to_path_buf()));
}
if path.join(".git").exists() {
return Some(("git", path.to_path_buf()));
}
if let Some(ancestor) = path.ancestors().find(|p| p.join(".git").exists()) {
return Some(("git", ancestor.to_path_buf()));
}
None
}
pub fn detect(path: &Path) -> Result<Box<dyn Vcs>> {
if path.join(".jj").is_dir()
&& let Ok(root) = jj::get_repo_root(path)
{
return Ok(Box::new(jj::JjVcs::new(root)?));
}
if let Some(ancestor) = path.ancestors().find(|p| p.join(".jj").is_dir())
&& let Ok(root) = jj::get_repo_root(ancestor)
{
return Ok(Box::new(jj::JjVcs::new(root)?));
}
if let Ok(root) = git::get_repo_root(path) {
return Ok(Box::new(git::GitVcs::new(root)?));
}
anyhow::bail!("Not a git or jj repository")
}
pub trait Vcs: Send + Sync {
fn repo_path(&self) -> &Path;
fn comparison_context(&self) -> Result<ComparisonContext>;
fn refresh(&self, cancel_flag: &Arc<AtomicBool>) -> Result<RefreshResult>;
fn single_file_diff(&self, file_path: &str) -> Option<FileDiff>;
fn base_identifier(&self) -> Result<String>;
fn base_file_bytes(&self, file_path: &str) -> Result<Option<Vec<u8>>>;
fn working_file_bytes(&self, file_path: &str) -> Result<Option<Vec<u8>>>;
fn binary_files(&self) -> HashSet<String>;
fn fetch(&self) -> Result<()>;
fn has_conflicts(&self) -> Result<bool>;
fn is_locked(&self) -> bool;
fn watch_paths(&self) -> VcsWatchPaths;
fn classify_event(&self, path: &Path) -> VcsEventType;
fn backend(&self) -> VcsBackend;
fn current_revision_id(&self) -> Result<String>;
fn set_diff_base(&self, _base: DiffBase) {}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn detect_repo_dir_empty() {
let tmp = tempfile::tempdir().expect("tempdir");
assert!(detect_repo_dir(tmp.path()).is_none());
}
#[test]
fn detect_repo_dir_jj() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::create_dir(tmp.path().join(".jj")).expect("mkdir .jj");
let result = detect_repo_dir(tmp.path());
assert_eq!(result, Some(("jj", tmp.path().to_path_buf())));
}
#[test]
fn detect_repo_dir_git() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::create_dir(tmp.path().join(".git")).expect("mkdir .git");
let result = detect_repo_dir(tmp.path());
assert_eq!(result, Some(("git", tmp.path().to_path_buf())));
}
#[test]
fn detect_repo_dir_jj_takes_precedence() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::create_dir(tmp.path().join(".jj")).expect("mkdir .jj");
fs::create_dir(tmp.path().join(".git")).expect("mkdir .git");
let (vcs_type, _) = detect_repo_dir(tmp.path()).expect("should detect");
assert_eq!(vcs_type, "jj");
}
#[test]
fn detect_repo_dir_git_worktree_file() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::write(tmp.path().join(".git"), "gitdir: /some/other/repo/.git/worktrees/wt")
.expect("write .git file");
let result = detect_repo_dir(tmp.path());
assert_eq!(result, Some(("git", tmp.path().to_path_buf())));
}
#[test]
fn detect_repo_dir_ancestor() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::create_dir(tmp.path().join(".jj")).expect("mkdir .jj");
let child = tmp.path().join("subdir");
fs::create_dir(&child).expect("mkdir subdir");
let result = detect_repo_dir(&child);
assert_eq!(result, Some(("jj", tmp.path().to_path_buf())));
}
}