#[cfg(any(target_os = "linux", target_os = "macos"))]
#[path = "worktree_destination.rs"]
mod destination;
use crate::{Caveats, Scope};
use std::path::{Path, PathBuf};
pub(crate) fn task_path_literal(path: &Path) -> String {
let path = dunce::simplified(path).to_string_lossy();
let longest = path.split(|c| c != '`').map(str::len).max().unwrap_or(0);
let delimiter = "`".repeat(longest + 1);
let pad = if path.starts_with('`')
|| path.ends_with('`')
|| (path.starts_with(' ') && path.ends_with(' ') && !path.chars().all(|c| c == ' '))
{
" "
} else {
""
};
format!("{delimiter}{pad}{path}{pad}{delimiter}")
}
#[derive(Debug, Default)]
pub struct WorktreeSession {
adopted: std::sync::Mutex<Option<AdoptedWorktree>>,
pub(crate) report: std::sync::Arc<std::sync::Mutex<crate::agentic::observed_report::State>>,
task_worktree: std::sync::Mutex<Option<(PathBuf, String)>>,
pub(crate) command_cwd_notice_shown: std::sync::atomic::AtomicBool,
pub(crate) publish_early: std::sync::Mutex<crate::agentic::publish_early::State>,
pub(crate) branch_nudge_shown: std::sync::atomic::AtomicBool,
}
impl WorktreeSession {
pub fn lift(&self) {
*self.report.lock().expect("report state") = Default::default();
*self.adopted.lock().expect("worktree session lock") = None;
*self.task_worktree.lock().expect("worktree hint lock") = None;
self.command_cwd_notice_shown
.store(false, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn record_task_worktree(&self, path: &Path, branch: &str) {
*self.task_worktree.lock().expect("worktree hint lock") =
Some((path.to_owned(), branch.to_owned()));
}
pub(crate) fn task_hint(&self) -> Option<String> {
self.task_worktree.lock().expect("worktree hint lock").as_ref().map(|(path, branch)| format!(
"Task worktree: {} (branch {branch}) — run commands there, not in the original checkout.",
task_path_literal(path)
))
}
pub(crate) fn task_root(&self, workspace: &Path) -> Option<PathBuf> {
let path = self
.task_worktree
.lock()
.expect("worktree hint lock")
.as_ref()?
.0
.clone();
Self::bound_task_root(&path, workspace)
}
pub(crate) fn bound_task_root(path: &Path, workspace: &Path) -> Option<PathBuf> {
let root = path.canonicalize().ok()?;
let gitfile = root.join(".git");
if !std::fs::symlink_metadata(&gitfile).ok()?.is_file() {
return None;
}
let common = common_git_dir(workspace)?;
let admin = crate::workspace_key::resolve_gitdir_file(&gitfile, &root)?
.canonicalize()
.ok()?;
if !admin.is_dir() || admin.parent()? != common.join("worktrees") {
return None;
}
let backlink = std::fs::read_to_string(admin.join("gitdir")).ok()?;
if backlink.trim().is_empty()
|| admin.join(backlink.trim()).canonicalize().ok()? != gitfile.canonicalize().ok()?
{
return None;
}
Some(root)
}
pub(crate) fn snapshot(&self) -> Option<AdoptedWorktree> {
self.adopted.lock().expect("worktree session lock").clone()
}
pub(crate) fn adopt(&self, candidate: AdoptedWorktree) {
let mut state = self.adopted.lock().expect("worktree session lock");
if state.is_none() {
*state = Some(candidate);
}
}
}
pub(crate) fn common_git_dir(workspace: &Path) -> Option<PathBuf> {
let admin = crate::workspace_key::discover_git_dir(workspace)?
.canonicalize()
.ok()?;
match std::fs::read_to_string(admin.join("commondir")) {
Ok(text) if !text.trim().is_empty() => admin.join(text.trim()).canonicalize().ok(),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Some(admin),
_ => None,
}
}
#[derive(Clone, Debug)]
pub(crate) struct AdoptedWorktree {
original: PathBuf,
pub(crate) worktree: PathBuf,
pub(crate) task_branch: Option<String>,
common: PathBuf,
admin: PathBuf,
protected_branch: Option<String>,
relocate_workspace_access: bool,
}
fn resolved(path: &Path) -> Option<PathBuf> {
crate::config::resolve_uncreated_path(path).ok()
}
fn overlap(a: &Path, b: &Path) -> bool {
a.starts_with(b) || b.starts_with(a)
}
impl AdoptedWorktree {
pub(crate) fn task_authority(&self, base: &Caveats) -> Caveats {
let mut out = base.clone();
if self.relocate_workspace_access {
for scope in [&mut out.fs_read, &mut out.fs_write] {
if crate::caveats::permits_path(scope, &self.original.to_string_lossy()) {
if let Scope::Only(roots) = scope {
roots.insert(self.worktree.to_string_lossy().into_owned());
}
}
}
}
out
}
pub(crate) fn valid(&self, caveats: &Caveats) -> bool {
self.worktree.canonicalize().ok().as_ref() == Some(&self.worktree)
&& crate::git_staging::HeldRoots::bind(&caveats.fs_read)
.ok()
.and_then(|held| crate::git_staging::discover_git_dirs(&self.worktree, &held).ok())
.is_some_and(|(common, admin)| common == self.common && admin == self.admin)
}
pub(crate) fn protects_branch(&self, branch: &str) -> bool {
self.protected_branch.as_deref() == Some(branch)
}
pub(crate) fn create_branch(
&self,
cwd: &Path,
branch: &str,
authority: &Caveats,
) -> Result<(), String> {
let held =
crate::git_staging::HeldRoots::bind(&authority.fs_read).map_err(|e| e.to_string())?;
let dirs = crate::git_staging::discover_git_dirs(cwd, &held).map_err(|e| e.to_string())?;
if !self.valid(authority)
|| dirs != (self.common.clone(), self.admin.clone())
|| !cwd
.canonicalize()
.is_ok_and(|p| p.starts_with(&self.worktree))
|| !crate::caveats::permits_path(&authority.fs_write, &self.worktree.to_string_lossy())
|| self.protects_branch(branch)
{
return Err(self.notice());
}
crate::git_hardening::create_worktree_branch(&self.worktree, branch)
}
fn exception(&self, path: &Path) -> bool {
path.starts_with(&self.worktree)
|| path.starts_with(&self.admin)
|| path.starts_with(self.common.join("objects"))
}
pub(crate) fn blocked(&self, path: &Path) -> bool {
resolved(path).is_none_or(|p| {
(overlap(&p, &self.original) || overlap(&p, &self.common)) && !self.exception(&p)
})
}
pub(crate) fn notice(&self) -> String {
format!("capability denied: the original checkout is read-only after worktree adoption; write in {} instead. Shared Git config stays read-only; use git -c user.name=… -c user.email=… for per-command identity. Only the operator can lift this with /permissions worktree-lift.", task_path_literal(&self.worktree))
}
pub(crate) fn attenuate(&self, authority: &Caveats) -> Caveats {
let mut out = authority.clone();
let mut roots = std::collections::BTreeSet::new();
if let Scope::Only(grants) = &authority.fs_write {
for root in grants {
if let Some(path) = resolved(Path::new(root)) {
if (!overlap(&path, &self.original) && !overlap(&path, &self.common))
|| self.exception(&path)
{
roots.insert(path.to_string_lossy().into_owned());
}
}
}
}
for root in [&self.worktree, &self.admin, &self.common.join("objects")] {
if crate::caveats::permits_path(&authority.fs_write, &root.to_string_lossy()) {
roots.insert(root.to_string_lossy().into_owned());
}
}
out.fs_write = Scope::only(roots);
out
}
}
pub(crate) struct Creation {
original: PathBuf,
destination: PathBuf,
common: PathBuf,
read: Scope<String>,
protected_branch: Option<String>,
relocate_workspace_access: bool,
#[cfg(any(target_os = "linux", target_os = "macos"))]
path_guard: destination::Destination,
}
impl Creation {
pub(crate) fn nested_in_original(&self) -> bool {
self.destination.starts_with(&self.original)
}
pub(crate) fn destination(&self) -> &Path {
&self.destination
}
pub(crate) fn prepare(&mut self, authority: &Caveats) -> Result<(), String> {
if ![&authority.fs_read, &authority.fs_write]
.iter()
.all(|scope| crate::caveats::permits_path(scope, &self.destination.to_string_lossy()))
{
return Err("worktree destination needs read and write approval".into());
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
self.path_guard.prepare()?;
self.read = authority.fs_read.clone();
self.relocate_workspace_access = true;
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
Err("safe worktree destination preparation is unavailable on this platform".into())
}
pub(crate) fn before(original: &Path, destination: &Path, caveats: &Caveats) -> Option<Self> {
let original = original.canonicalize().ok()?;
let original = original
.ancestors()
.find(|dir| dir.join(".git").exists())?
.to_path_buf();
let destination = resolved(destination)?;
if destination.join(".git").exists() {
return None;
}
let held = crate::git_staging::HeldRoots::bind(&caveats.fs_read).ok()?;
let (common, admin) = crate::git_staging::discover_git_dirs(&original, &held).ok()?;
let head = held.read_to_string(&admin, Path::new("HEAD")).ok()??;
let protected_branch = if let Some(branch) = head.trim().strip_prefix("ref: refs/heads/") {
crate::git_staging::validate_branch_name(branch).ok()?;
Some(branch.to_owned())
} else if crate::git_staging::is_hex_oid(head.trim()) {
None
} else {
return None;
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
let path_guard = destination::Destination::capture(&destination).ok()?;
Some(Self {
original,
destination,
common,
read: caveats.fs_read.clone(),
protected_branch,
relocate_workspace_access: false,
#[cfg(any(target_os = "linux", target_os = "macos"))]
path_guard,
})
}
pub(crate) fn matches_source(&self, cwd: &Path) -> bool {
crate::git_staging::HeldRoots::bind(&self.read)
.ok()
.and_then(|held| crate::git_staging::discover_git_dirs(cwd, &held).ok())
.is_some_and(|(common, _)| common == self.common)
}
pub(crate) fn ready(&self) -> Result<(), String> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
self.path_guard.check()?;
Ok(())
}
pub(crate) fn leftover_notice(&self) -> Option<String> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
if self.path_guard.left_empty() {
return Some(format!(
"left empty directory {}; remove it if unwanted",
task_path_literal(&self.destination)
));
}
None
}
pub(crate) fn verify(self) -> Option<AdoptedWorktree> {
self.ready().ok()?;
let held = crate::git_staging::HeldRoots::bind(&self.read).ok()?;
let (common, admin) =
crate::git_staging::discover_git_dirs(&self.destination, &held).ok()?;
if common != self.common || admin == common || !admin.starts_with(common.join("worktrees"))
{
return None;
}
let gitlink = self.destination.join(".git");
if !gitlink.is_file() {
return None;
}
let back = held.read_to_string(&admin, Path::new("gitdir")).ok()??;
if resolved(Path::new(back.trim()))? != gitlink.canonicalize().ok()? {
return None;
}
self.ready().ok()?;
Some(AdoptedWorktree {
original: self.original,
worktree: self.destination,
task_branch: crate::git_staging::read_head_branch(&admin, &held).ok(),
common,
admin,
protected_branch: self.protected_branch,
relocate_workspace_access: self.relocate_workspace_access,
})
}
}
#[cfg(test)]
#[path = "worktree_adoption_tests.rs"]
pub(crate) mod tests;