use std::collections::HashMap;
use std::fs::{self, File};
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{LazyLock, Mutex};
pub const DEFAULT_SHARE: &[&str] = &[
".uv-cache",
".next",
".turbo",
".nuxt",
".svelte-kit",
".parcel-cache",
".gradle",
".mypy_cache",
".ruff_cache",
".pytest_cache",
];
static PROBES: LazyLock<Mutex<HashMap<(PathBuf, PathBuf), bool>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn err(msg: String) -> io::Error {
io::Error::other(msg)
}
#[derive(Clone, Debug)]
pub struct Options {
pub share: Vec<String>,
pub warm_interval: std::time::Duration,
}
impl Default for Options {
fn default() -> Self {
Options {
share: DEFAULT_SHARE.iter().map(|s| s.to_string()).collect(),
warm_interval: std::time::Duration::from_secs(3600),
}
}
}
impl Options {
pub fn from_repo(repo: &Path) -> Options {
let mut opts = Options::default();
let extra = git(repo, &["config", "--get-all", "cow.share"]).unwrap_or_default();
for d in extra.lines().map(str::trim).filter(|d| !d.is_empty()) {
if !opts.share.iter().any(|l| l == d) {
opts.share.push(d.to_string());
}
}
if let Some(i) = git(repo, &["config", "--get", "cow.warmInterval"])
.ok()
.and_then(|v| parse_interval(&v))
{
opts.warm_interval = i;
}
opts
}
}
fn parse_interval(v: &str) -> Option<std::time::Duration> {
let v = v.trim();
if v == "never" {
return Some(std::time::Duration::MAX);
}
let (n, unit) = match v.strip_suffix('h') {
Some(n) => (n, 3600),
None => match v.strip_suffix('m') {
Some(n) => (n, 60),
None => (v.strip_suffix('s').unwrap_or(v), 1),
},
};
n.trim()
.parse::<u64>()
.ok()
.map(|n| std::time::Duration::from_secs(n * unit))
}
#[cfg(target_os = "macos")]
fn clone_into(src: &Path, dst_dir: &Path) -> io::Result<()> {
use std::ffi::{CString, c_char, c_int};
use std::os::unix::ffi::OsStrExt;
unsafe extern "C" {
fn clonefile(src: *const c_char, dst: *const c_char, flags: u32) -> c_int;
}
const CLONE_NOFOLLOW: u32 = 1;
let name = src
.file_name()
.ok_or_else(|| err(format!("{} has no name", src.display())))?;
let s = CString::new(src.as_os_str().as_bytes()).map_err(io::Error::other)?;
let d = CString::new(dst_dir.join(name).as_os_str().as_bytes()).map_err(io::Error::other)?;
if unsafe { clonefile(s.as_ptr(), d.as_ptr(), CLONE_NOFOLLOW) } != 0 {
let e = io::Error::last_os_error();
return Err(err(format!("clonefile {}: {e}", src.display())));
}
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn clone_into(src: &Path, dst_dir: &Path) -> io::Result<()> {
let out = Command::new("cp")
.args(["-a", "--reflink=always"])
.arg(src)
.arg(dst_dir)
.output()?;
if !out.status.success() {
return Err(err(format!(
"cp {}: {}",
src.display(),
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(())
}
pub fn git(dir: &Path, args: &[&str]) -> io::Result<String> {
let out = Command::new("git").arg("-C").arg(dir).args(args).output()?;
if !out.status.success() {
return Err(err(format!(
"git {args:?}: {}",
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
pub fn can_clone(src_dir: &Path, dst_dir: &Path) -> bool {
let key = (src_dir.to_path_buf(), dst_dir.to_path_buf());
let mut probes = PROBES.lock().unwrap();
if let Some(known) = probes.get(&key) {
return *known;
}
let id = std::process::id();
let src = src_dir.join(format!(".cow-probe-{id}"));
let into = dst_dir.join(format!(".cow-probe-dir-{id}"));
let ok = fs::write(&src, b"x").is_ok()
&& fs::create_dir_all(&into).is_ok()
&& clone_into(&src, &into).is_ok();
let _ = fs::remove_file(&src);
let _ = fs::remove_dir_all(&into);
probes.insert(key, ok);
ok
}
fn beside(template: &Path, ext: &str) -> PathBuf {
let name = template.file_name().unwrap_or_default().to_string_lossy();
template.with_file_name(format!(".{name}.{ext}"))
}
fn lock_template(template: &Path) -> io::Result<File> {
let file = File::create(beside(template, "lock"))?;
file.lock()?;
Ok(file)
}
fn refresh_template(repo: &Path, template: &Path, start: &str) -> io::Result<()> {
if template.join(".git").exists()
&& git(template, &["checkout", "--detach", "--force", start]).is_ok()
{
return Ok(());
}
let _ = fs::remove_dir_all(template);
if clone_checkout(repo, template, start).is_ok() {
return Ok(());
}
let _ = fs::remove_dir_all(template);
git(
repo,
&[
"worktree",
"add",
"-f",
"--detach",
&template.to_string_lossy(),
start,
],
)?;
Ok(())
}
fn clone_checkout(repo: &Path, template: &Path, start: &str) -> io::Result<()> {
let top = PathBuf::from(git(repo, &["rev-parse", "--show-toplevel"])?);
let parent = template
.parent()
.ok_or_else(|| err("template has no parent".into()))?;
if !can_clone(&top, parent) {
return Err(err("checkout and template cannot share blocks".into()));
}
git(
repo,
&[
"worktree",
"add",
"-f",
"--no-checkout",
"--detach",
&template.to_string_lossy(),
start,
],
)?;
for name in git(&top, &["ls-tree", "--name-only", "HEAD"])?.lines() {
let src = top.join(name);
if src.symlink_metadata().is_ok() {
clone_into(&src, template)?;
}
}
let src_git = git(&top, &["rev-parse", "--absolute-git-dir"])?;
let tpl_git = git(template, &["rev-parse", "--absolute-git-dir"])?;
fs::copy(
Path::new(&src_git).join("index"),
Path::new(&tpl_git).join("index"),
)?;
let _ = git(template, &["update-index", "-q", "--refresh"]);
git(template, &["reset", "--hard", "-q"])?;
git(template, &["clean", "-fdxq"])?;
Ok(())
}
fn ignored_dirs(src: &Path, share: &[String]) -> io::Result<Vec<String>> {
let out = git(
src,
&[
"ls-files",
"--others",
"--ignored",
"--exclude-standard",
"--directory",
],
)?;
Ok(out
.lines()
.filter_map(|l| l.strip_suffix('/'))
.filter(|n| {
!n.is_empty()
&& n.split('/')
.all(|c| !c.starts_with('.') || share.iter().any(|s| s == c))
})
.map(String::from)
.collect())
}
fn warm_template(src: &Path, template: &Path, share: &[String]) -> io::Result<()> {
let staging = beside(template, "warm");
for dir in ignored_dirs(src, share)? {
if !git(template, &["ls-files", "--", &dir])
.unwrap_or_default()
.is_empty()
{
continue;
}
let _ = fs::remove_dir_all(&staging);
fs::create_dir_all(&staging)?;
let _ = (|| {
clone_into(&src.join(&dir), &staging)?;
let dst = template.join(&dir);
let _ = fs::remove_dir_all(&dst);
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)?;
}
let base = Path::new(&dir).file_name().unwrap_or_default();
fs::rename(staging.join(base), dst)
})();
let _ = fs::remove_dir_all(&staging);
}
Ok(())
}
pub fn warm(src: &Path, template: &Path) -> io::Result<()> {
warm_with(src, template, &Options::default())
}
pub fn warm_with(src: &Path, template: &Path, opts: &Options) -> io::Result<()> {
if !template.join(".git").exists() {
return Err(err(format!("no template at {}", template.display())));
}
let _lock = lock_template(template)?;
warm_template(src, template, &opts.share)?;
stamp(template)
}
pub fn remove_template(repo: &Path, template: &Path) -> io::Result<()> {
if template.parent().is_none_or(|p| !p.exists()) {
return Ok(());
}
let _lock = lock_template(template)?;
if template.join(".git").exists() {
git(
repo,
&["worktree", "remove", "--force", &template.to_string_lossy()],
)?;
}
let _ = fs::remove_dir_all(template);
let _ = fs::remove_dir_all(beside(template, "warm"));
for ext in ["warmed", "lock"] {
match fs::remove_file(beside(template, ext)) {
Err(e) if e.kind() != io::ErrorKind::NotFound => return Err(e),
_ => {}
}
}
Ok(())
}
fn stamp_path(template: &Path) -> PathBuf {
beside(template, "warmed")
}
fn stamp(template: &Path) -> io::Result<()> {
fs::write(stamp_path(template), b"")
}
fn warmed_within(template: &Path, interval: std::time::Duration) -> bool {
fs::metadata(stamp_path(template))
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.elapsed().ok())
.is_some_and(|age| age < interval)
}
fn populate(
repo: &Path,
template: &Path,
wt: &Path,
new_branch: Option<&str>,
commitish: Option<&str>,
) -> io::Result<()> {
let wt_s = wt.to_string_lossy();
let mut args = vec!["worktree", "add", "--no-checkout"];
if let Some(b) = new_branch {
args.extend(["-b", b]);
}
args.push(&wt_s);
args.extend(commitish);
git(repo, &args)?;
for entry in fs::read_dir(template)? {
let entry = entry?;
if entry.file_name() == ".git" {
continue;
}
clone_into(&entry.path(), wt)?;
}
let tpl_git = git(template, &["rev-parse", "--absolute-git-dir"])?;
let wt_git = git(wt, &["rev-parse", "--absolute-git-dir"])?;
fs::copy(
Path::new(&tpl_git).join("index"),
Path::new(&wt_git).join("index"),
)?;
let _ = git(wt, &["update-index", "-q", "--refresh"]);
git(wt, &["reset", "--hard", "-q"])?;
Ok(())
}
pub fn add(
repo: &Path,
template: &Path,
template_start: &str,
wt: &Path,
new_branch: Option<&str>,
commitish: Option<&str>,
) -> io::Result<()> {
add_with(
repo,
template,
template_start,
wt,
new_branch,
commitish,
&Options::default(),
)
}
pub fn add_with(
repo: &Path,
template: &Path,
template_start: &str,
wt: &Path,
new_branch: Option<&str>,
commitish: Option<&str>,
opts: &Options,
) -> io::Result<()> {
let wt_parent = wt
.parent()
.ok_or_else(|| err("worktree has no parent".into()))?;
let tpl_parent = template
.parent()
.ok_or_else(|| err("template has no parent".into()))?;
fs::create_dir_all(wt_parent)?;
fs::create_dir_all(tpl_parent)?;
if !can_clone(tpl_parent, wt_parent) {
return Err(err("volume cannot clone files".into()));
}
let _lock = lock_template(template)?;
refresh_template(repo, template, template_start)?;
if template.join(".gitmodules").exists() {
return Err(err("submodules are not supported".into()));
}
let never = opts.warm_interval == std::time::Duration::MAX;
if !never && (opts.warm_interval.is_zero() || !warmed_within(template, opts.warm_interval)) {
let _ = warm_template(repo, template, &opts.share);
let _ = stamp(template);
}
let ours = new_branch
.filter(|b| git(repo, &["rev-parse", "--verify", &format!("refs/heads/{b}")]).is_err());
let res = populate(repo, template, wt, new_branch, commitish);
if res.is_err() {
let _ = git(
repo,
&["worktree", "remove", "--force", &wt.to_string_lossy()],
);
let _ = fs::remove_dir_all(wt);
if let Some(b) = ours {
let _ = git(repo, &["branch", "-D", b]);
}
}
res
}
#[derive(Debug)]
pub enum Added {
Cloned,
Plain(io::Error),
}
pub fn add_or_plain(
repo: &Path,
template: &Path,
template_start: &str,
wt: &Path,
new_branch: Option<&str>,
commitish: Option<&str>,
opts: &Options,
) -> io::Result<Added> {
let Err(why) = add_with(
repo,
template,
template_start,
wt,
new_branch,
commitish,
opts,
) else {
return Ok(Added::Cloned);
};
let wt_s = wt.to_string_lossy();
let mut args = vec!["worktree", "add"];
if let Some(b) = new_branch {
args.extend(["-b", b]);
}
args.push(&wt_s);
args.extend(commitish);
git(repo, &args).map_err(|e| err(format!("{e}, after cloning failed: {why}")))?;
Ok(Added::Plain(why))
}