use super::*;
use crate::worktree_adoption::{task_path_literal, AdoptedWorktree, Creation};
use crate::Caveats;
use std::path::{Path, PathBuf};
use super::native_git::{invocation, is_git as resembles_git, literal};
pub(super) mod administration;
pub(super) mod branch;
mod detect;
#[path = "worktree_git.rs"]
mod git_identity;
mod nudge;
mod prepare;
fn worktree_add_args(words: &[String]) -> Option<&[String]> {
let (verb, args, _) = invocation(words).ok()?;
let (action, args) = args.split_first()?;
(verb == "worktree" && action == "add").then_some(args)
}
pub(super) fn creation_failure_hint(
source: &str,
envelope: &serde_json::Value,
) -> Option<&'static str> {
if shell::envelope_outcome(envelope) != crate::ExecOutcome::Failed {
return None;
}
let words = standalone_git_words(source)?;
let mut global = words.iter().skip(1);
let mut word = global.next()?;
while word == "-C" {
global.next()?;
word = global.next()?;
}
if word != "worktree" {
return None;
}
let args = worktree_add_args(&words)?;
let args: Vec<_> = args.iter().map(String::as_str).collect();
let branch = match args.as_slice() {
[path, branch] if !path.starts_with('-') => *branch,
["-b" | "-B", branch, _] | ["-b" | "-B", branch, _, _] => *branch,
_ => return None,
};
let stderr = envelope.get("stderr")?.as_str()?;
let invalid_reference = format!("fatal: invalid reference: {branch}");
if args.len() == 2 && stderr.lines().any(|line| line == invalid_reference) {
return Some("\nWorktree hint: To create a new branch for the worktree, use `git worktree add -b <name> <path>`.");
}
let prefix = format!("fatal: '{branch}' is already used by worktree at ");
stderr.lines().any(|line| line.starts_with(&prefix)).then_some(
"\nWorktree hint: if you switched the original to that branch, switch it back with `git switch -`. To create a new task branch, pick a new branch name and run `git worktree add -b <new-branch> <path> [<start>]` as a standalone command."
)
}
fn standalone_git_words(source: &str) -> Option<Vec<String>> {
let inspection = agent_bridle::inspect_shell(source).ok()?;
let [command] = inspection.commands.as_slice() else {
return None;
};
if !inspection.constructs.is_empty()
|| !inspection.warnings.is_empty()
|| !command.descendant_execs.is_empty()
|| !command.redirects.is_empty()
|| source.trim() != command.source.trim()
|| !command.program.as_deref().is_some_and(resembles_git)
{
return None;
}
let mut remaining = command.source.as_str();
for word in &command.argv {
remaining = remaining.trim_start().strip_prefix(word)?;
}
if !remaining.trim().is_empty() {
return None;
}
let words = command
.argv
.iter()
.map(|word| literal(word))
.collect::<Option<Vec<_>>>()?;
if words.iter().any(|word| word.contains(['\n', '\r'])) {
return None;
}
Some(words)
}
fn possible_creation(source: &str) -> bool {
fn words(program: Option<&str>, argv: &[String]) -> bool {
program.is_some_and(resembles_git)
&& argv.windows(2).any(|pair| {
literal(&pair[0]).as_deref() == Some("worktree")
&& literal(&pair[1]).as_deref() == Some("add")
})
}
fn contains(inspection: &agent_bridle::ShellInspection) -> bool {
inspection.commands.iter().any(|command| {
words(command.program.as_deref(), &command.argv)
|| command
.descendant_execs
.iter()
.any(|child| words(Some(&child.program), &child.argv))
}) || inspection
.constructs
.iter()
.any(|construct| construct.inspection.as_deref().is_some_and(contains))
}
match agent_bridle::inspect_shell(source) {
Ok(inspection) => contains(&inspection),
Err(_) => {
static MARKER: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"(?is)\bgit(?:\.exe)?\b.*\bworktree\b.*\badd\b")
.expect("fixed creation marker")
});
let text: String = source
.replace("\\\n", "")
.chars()
.filter(|c| !matches!(c, '\'' | '"' | '\\'))
.collect();
MARKER.is_match(&text)
}
}
}
fn creation_admission(
bypass: bool,
name: &str,
args: &serde_json::Value,
workspace: &str,
caveats: &Caveats,
engine: crate::ShellEngine,
trusted_git: impl FnOnce(&Caveats) -> Result<PathBuf, ()>,
) -> Result<Option<(Creation, String)>, ()> {
if bypass
|| name != "run_command"
|| !args
.get("command")
.and_then(|v| v.as_str())
.is_some_and(possible_creation)
{
return Ok(None);
}
if !creation_batch_is_read_only_after_add(args, engine) {
return Err(());
}
let candidate = creation(name, args, workspace, caveats).ok_or(())?;
let source = git_identity::pin(
args["command"].as_str().ok_or(())?,
&trusted_git(caveats)?,
caveats,
)?;
Ok(Some((candidate, source)))
}
fn creation(
name: &str,
args: &serde_json::Value,
workspace: &str,
caveats: &Caveats,
) -> Option<Creation> {
if name != "run_command" {
return None;
}
let (cd, cmd) = split_leading_cd(args.get("command")?.as_str()?);
let cwd = resolve_exec_cwd(workspace, args.get("cwd").and_then(|v| v.as_str()));
let cwd = resolve_exec_cwd(&cwd, cd.as_deref());
let inspection = agent_bridle::inspect_shell(&cmd).ok()?;
for command in inspection.commands {
if command.program.as_deref() == Some("cd") {
return None;
}
if command.program.as_deref()? != "git" {
continue;
}
let words = command
.argv
.iter()
.map(|s| git_identity::argument(s))
.collect::<Option<Vec<_>>>()?;
let Some(add_args) = worktree_add_args(&words) else {
continue;
};
let mut args = words.iter().skip(1);
let mut git_cwd = PathBuf::from(&cwd);
let mut word = args.next()?;
while word == "-C" {
git_cwd = git_cwd.join(args.next()?);
word = args.next()?;
}
if word != "worktree" {
return None; }
let mut args = add_args.iter();
let mut destination = None;
while let Some(word) = args.next() {
match word.as_str() {
"-b" | "-B" | "--reason" => {
args.next()?;
}
"--" => {
destination = args.next();
break;
}
"-f" | "--force" | "-d" | "--detach" | "--checkout" | "--no-checkout"
| "--lock" | "-q" | "--quiet" => {}
flag if flag.starts_with('-') => return None,
_ => {
destination = Some(word);
break;
}
}
}
let candidate =
Creation::before(Path::new(workspace), &git_cwd.join(destination?), caveats)?;
if !candidate.matches_source(&git_cwd) {
return None;
}
return Some(candidate);
}
None
}
fn display_builtin(program: &str, engine: crate::ShellEngine) -> bool {
matches!(program, "echo" | "printf" | "pwd") && engine != crate::ShellEngine::SafeSubset
}
fn creation_batch_is_read_only_after_add(
args: &serde_json::Value,
engine: crate::ShellEngine,
) -> bool {
let Some(command) = args.get("command").and_then(|v| v.as_str()) else {
return false;
};
let (_, command) = split_leading_cd(command);
let Ok(inspection) = agent_bridle::inspect_shell(&command) else {
return false;
};
if !inspection.constructs.is_empty() {
return false; }
let mut additions = 0;
let safe = inspection.commands.iter().all(|c| {
if !c.descendant_execs.is_empty()
|| !c
.argv
.first()
.is_some_and(|word| c.source.starts_with(word))
{
return false;
}
let Some(words) = c
.argv
.iter()
.map(|s| git_identity::argument(s))
.collect::<Option<Vec<_>>>()
else {
return false;
};
let args: Vec<_> = words.iter().skip(1).map(String::as_str).collect();
match c.program.as_deref() {
Some("git") => {
if worktree_add_args(&words).is_some() {
additions += 1;
c.redirects.iter().all(|r| {
r.fd == Some(2)
&& r.operation == agent_bridle::RedirectOperation::DuplicateOutput
&& r.target == "1"
})
} else {
c.redirects.is_empty() && git_identity::read_only(&args)
}
}
Some("tail") => {
c.redirects.is_empty()
&& super::super::routing::parse_trim_spec(&words.join(" ")).is_some()
}
Some(program) => c.redirects.is_empty() && display_builtin(program, engine),
None => false,
}
});
safe && additions == 1
}
struct Guard<'p, 'g> {
policy: &'p AdoptedWorktree,
inner: Option<&'g mut dyn PermissionGate>,
}
impl Guard<'_, '_> {
fn decision(&self, decision: PermissionDecision) -> PermissionDecision {
match decision {
PermissionDecision::Allow(c) => PermissionDecision::Allow(self.policy.attenuate(&c)),
PermissionDecision::Deny => PermissionDecision::Deny,
}
}
fn blocked(&self, requests: &[PermissionRequest]) -> bool {
requests.iter().any(|r| {
matches!(r.kind, DenialKind::FsWrite | DenialKind::Build)
&& self.policy.blocked(Path::new(&r.target))
})
}
}
impl PermissionGate for Guard<'_, '_> {
fn refresh_caveats(&mut self, baseline: &Caveats) -> PermissionDecision {
let decision = self.inner.as_deref_mut().map_or_else(
|| PermissionDecision::Allow(baseline.clone()),
|g| g.refresh_caveats(baseline),
);
self.decision(decision)
}
fn ask(&mut self, requests: &[PermissionRequest]) -> PermissionDecision {
if self.blocked(requests) {
return PermissionDecision::Deny;
}
let decision = self
.inner
.as_deref_mut()
.map_or(PermissionDecision::Deny, |g| g.ask(requests));
self.decision(decision)
}
fn ask_with_caveats(
&mut self,
base: &Caveats,
requests: &[PermissionRequest],
) -> PermissionDecision {
if self.blocked(requests) {
return PermissionDecision::Deny;
}
let decision = self
.inner
.as_deref_mut()
.map_or(PermissionDecision::Deny, |g| {
g.ask_with_caveats(base, requests)
});
self.decision(decision)
}
fn ask_question(&mut self, q: &str) -> super::super::permissions::HumanQuestionOutcome {
self.inner.as_deref_mut().map_or(
super::super::permissions::HumanQuestionOutcome::Unavailable,
|g| g.ask_question(q),
)
}
fn consume_pending_once(&mut self, kind: DenialKind, target: &str) {
if let Some(g) = self.inner.as_deref_mut() {
g.consume_pending_once(kind, target);
}
}
fn queue_pending_once(&mut self, kind: DenialKind, target: &str) {
if let Some(g) = self.inner.as_deref_mut() {
g.queue_pending_once(kind, target);
}
}
fn queue_command_retry(&mut self, command: &str, cwd: &str, requests: &[PermissionRequest]) {
if let Some(g) = self.inner.as_deref_mut() {
g.queue_command_retry(command, cwd, requests);
}
}
fn apply_command_retry(&mut self, command: &str, cwd: &str, base: &Caveats) -> Caveats {
let c = self.inner.as_deref_mut().map_or_else(
|| base.clone(),
|g| g.apply_command_retry(command, cwd, base),
);
self.policy.attenuate(&c)
}
fn apply_pending_once(&mut self, kind: DenialKind, target: &str, base: &Caveats) -> Caveats {
let c = self.inner.as_deref_mut().map_or_else(
|| base.clone(),
|g| g.apply_pending_once(kind, target, base),
);
self.policy.attenuate(&c)
}
}
const UNARMED_NOTICE: &str = "Warning: worktree protection is not armed because OCAP is disabled; the original checkout remains writable under existing permissions. Use a confined session for automatic original-checkout protection.";
fn record_verified_creation(
session: &crate::worktree_adoption::WorktreeSession,
adopted: AdoptedWorktree,
bypass: bool,
) -> String {
if let Some(branch) = &adopted.task_branch {
session.record_task_worktree(&adopted.worktree, branch);
}
if bypass {
format!(
"Task worktree created: {}. {UNARMED_NOTICE}",
task_path_literal(&adopted.worktree)
)
} else {
let notice = format!(
"Adopted task worktree: {}. The original checkout and shared config are now read-only for this task. Use git -c user.name=… -c user.email=… for per-command identity. Any uncommitted changes in the original checkout are now read-only; copy them into the new worktree and commit there, or ask the operator for /permissions worktree-lift.",
task_path_literal(&adopted.worktree)
);
session.adopt(adopted);
notice
}
}
fn report_leftover(
presentation: &mut dyn ToolPresentation,
candidate: &Creation,
mut result: String,
) -> String {
if let Some(notice) = candidate.leftover_notice() {
presentation.preview(¬ice, 0);
result.push_str(&format!("\n{notice}"));
}
result
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn execute(
presentation: &mut dyn ToolPresentation,
name: &str,
args: &serde_json::Value,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &Caveats,
mcp: &mut dyn McpTools,
collab: ToolCollaborators<'_, '_>,
tool_offload: bool,
disposition: PromptDisposition,
) -> String {
let bypass = ocap_disabled();
let session = collab.worktree_session;
let task_root = session.and_then(|session| {
matches!(
name,
"read_file" | "write_file" | "edit_file" | "delete_file" | "list_dir" | "find" | "grep"
)
.then(|| session.task_root(Path::new(workspace)))
.flatten()
});
let absolute_search = matches!(name, "find" | "grep")
&& args
.get("path")
.and_then(|p| p.as_str())
.is_some_and(|p| Path::new(p).is_absolute());
let workspace = task_root
.as_deref()
.filter(|_| !absolute_search)
.and_then(Path::to_str)
.unwrap_or(workspace);
let objective = collab.prompt_context.map(|context| context.active_text());
let command_directory = collab.command_directory;
let policy = session.and_then(crate::worktree_adoption::WorktreeSession::snapshot);
let mut collab = collab;
let task_authority = if let Some(policy) = &policy {
let projected = policy.attenuate(&policy.task_authority(caveats));
let current = match collab.permission_gate.as_deref_mut() {
Some(gate) => match gate.refresh_caveats(&projected) {
PermissionDecision::Allow(current) => current,
PermissionDecision::Deny => {
if let Some(invocation) = collab.invocation {
invocation.host();
}
if let Some(slot) = collab.execution {
let _ = slot.set(crate::ExecOutcome::Denied);
}
return "capability denied: current permissions refuse task worktree access"
.into();
}
},
None => projected,
};
Some(policy.attenuate(¤t))
} else {
None
};
let caveats = task_authority.as_ref().unwrap_or(caveats);
let mut normalized = shell::command_args_with_default_cwd(
name,
args,
workspace,
collab.default_command_cwd,
collab.worktree_session,
)
.unwrap_or(std::borrow::Cow::Borrowed(args));
let command_default_notice = shell::command_cwd_key(name).and_then(|key| {
if args.get(key).is_some()
|| args
.get("command")
.and_then(|v| v.as_str())
.is_some_and(|cmd| shell::split_leading_cd(cmd).0.is_some())
{
return None;
}
let task = session?.task_root(Path::new(workspace))?;
let child_path = dunce::simplified(&task);
(normalized.get(key)?.as_str()? == child_path.to_str()?).then(|| {
format!(
"Commands now run in the task worktree {}",
task_path_literal(child_path)
)
})
});
let unavailable_creation = !bypass
&& name == "run_command"
&& (session.is_none() || policy.is_some())
&& normalized
.get("command")
.and_then(|v| v.as_str())
.is_some_and(possible_creation);
let admission = if unavailable_creation {
Err(())
} else {
session.filter(|_| policy.is_none()).map_or(Ok(None), |_| {
creation_admission(
bypass,
name,
&normalized,
workspace,
caveats,
shell::shell_engine(),
git_identity::resolve,
)
})
};
let Ok(candidate) = admission else {
if let Some(invocation) = collab.invocation {
invocation.host();
}
if let Some(slot) = collab.execution {
let _ = slot.set(crate::ExecOutcome::Denied);
}
let protection = if bypass {
"; OCAP is disabled, so automatic worktree protection will not be armed"
} else {
" so it can become read-only before other mutations run"
};
return format!("capability denied: cannot verify this worktree creation and its surrounding commands; run `git worktree add -b <new-branch> <path> [<start>]` as a standalone literal command in the original checkout{protection}. Do not create the branch in the original checkout first; -b creates it for the new worktree");
};
if candidate
.as_ref()
.is_some_and(|(candidate, _)| candidate.nested_in_original())
{
if let Some(invocation) = collab.invocation {
invocation.host();
}
if let Some(slot) = collab.execution {
let _ = slot.set(crate::ExecOutcome::Denied);
}
return "capability denied: the task worktree must be outside the original checkout; choose a sibling destination, for example `git worktree add -b <new-branch> ../<name> [<start>]`".into();
}
let mut candidate = candidate.map(|(candidate, source)| {
normalized.to_mut()["command"] = source.into();
candidate
});
let creation_authority = if let Some(candidate) = candidate.as_mut() {
match prepare::creation(
candidate,
&normalized,
workspace,
caveats,
&mut collab.permission_gate,
) {
Ok(authority) => Some(authority),
Err(reason) => {
if let Some(invocation) = collab.invocation {
invocation.host();
}
if let Some(slot) = collab.execution {
let _ = slot.set(crate::ExecOutcome::Denied);
}
return report_leftover(
presentation,
candidate,
format!("capability denied: {reason}"),
);
}
}
} else {
None
};
let caveats = creation_authority.as_ref().unwrap_or(caveats);
let args = normalized.as_ref();
let execution = collab.execution;
let publication = collab.governed_pr;
if let Some(candidate) = &candidate {
if let Err(reason) = candidate.ready() {
if let Some(invocation) = collab.invocation {
invocation.host();
}
if let Some(slot) = execution {
let _ = slot.set(crate::ExecOutcome::Denied);
}
return report_leftover(
presentation,
candidate,
format!("capability denied: {reason}"),
);
}
}
let before = (name == "run_command" && session.is_some())
.then(|| detect::Snapshot::before(Path::new(workspace), &caveats.fs_read))
.flatten();
let mut result = if let Some(policy) = &policy {
let refuse_git = name == "git"
&& !matches!(
args.get("op").and_then(|v| v.as_str()).unwrap_or(""),
"status" | "log" | "diff" | "show" | "branch-list" | "blame" | "rev-parse"
)
&& policy.blocked(
&Path::new(workspace).join(args.get("cwd").and_then(|v| v.as_str()).unwrap_or(".")),
);
let refuse = matches!(name, "write_file" | "edit_file" | "delete_file")
&& args
.get("path")
.and_then(|v| v.as_str())
.is_some_and(|p| policy.blocked(&Path::new(workspace).join(p)));
if refuse_git
|| !policy.valid(caveats)
|| refuse
|| (bypass && matches!(name, "run_command" | "lifecycle" | "build_exec"))
{
if let Some(invocation) = collab.invocation {
invocation.host();
}
if let Some(slot) = execution {
let _ = slot.set(crate::ExecOutcome::Denied);
}
return policy.notice();
}
let narrowed = policy.attenuate(caveats);
let mut gate = Guard {
policy,
inner: collab.permission_gate.take(),
};
Box::pin(execute_tool_unadopted(
presentation,
name,
args,
workspace,
color,
tool_output_lines,
&narrowed,
mcp,
ToolCollaborators {
permission_gate: Some(&mut gate),
..collab
},
tool_offload,
disposition,
))
.await
} else if let Some(candidate) = &candidate {
let mut guard = prepare::Guard {
candidate,
inner: collab.permission_gate.take(),
};
Box::pin(execute_tool_unadopted(
presentation,
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
ToolCollaborators {
permission_gate: Some(&mut guard),
..collab
},
tool_offload,
disposition,
))
.await
} else {
Box::pin(execute_tool_unadopted(
presentation,
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
collab,
tool_offload,
disposition,
))
.await
};
if let (Some(session), Some(notice)) = (session, command_default_notice) {
if !session
.command_cwd_notice_shown
.swap(true, std::sync::atomic::Ordering::Relaxed)
{
presentation.preview(¬ice, 0);
result.push_str(&format!("\n{notice}"));
}
}
if name == "run_command"
&& execution.and_then(|slot| slot.get()) == Some(&crate::ExecOutcome::Passed)
{
if let (Some(session), Some(objective), Some(raw)) = (
session,
objective,
normalized.get("command").and_then(|value| value.as_str()),
) {
let (cd, command) = shell::split_leading_cd(raw);
let base = shell::resolve_exec_cwd(
workspace,
normalized.get("cwd").and_then(|value| value.as_str()),
);
let fallback = PathBuf::from(shell::resolve_exec_cwd(&base, cd.as_deref()));
let cwd = command_directory
.and_then(|slot| slot.get())
.unwrap_or(&fallback);
if let Some(hint) =
nudge::branch_in_place(session, objective, &command, cwd, &caveats.fs_read)
{
result.push_str(&format!("\n{hint}"));
}
}
}
if bypass
&& session.is_some()
&& name == "run_command"
&& normalized
.get("command")
.and_then(|value| value.as_str())
.is_some_and(possible_creation)
{
if let Some(session) = session {
nudge::record_unarmed_creation(
session,
&normalized,
workspace,
execution.and_then(|slot| slot.get()),
);
}
presentation.preview(UNARMED_NOTICE, 0);
result.push_str(&format!("\n{UNARMED_NOTICE}"));
}
if let Some(candidate) = &candidate {
result = report_leftover(presentation, candidate, result);
}
if let (Some(session), Some(candidate)) = (session, candidate) {
if matches!(
execution.and_then(|slot| slot.get()),
Some(crate::ExecOutcome::Passed | crate::ExecOutcome::Failed)
) {
if let Some(adopted) = candidate.verify() {
let _ = crate::git_hardening::ambient_gitdir_write_grant(&adopted.worktree);
let notice = record_verified_creation(session, adopted, bypass);
if bypass {
presentation.preview(¬ice, 0);
}
result.push_str(&format!("\n{notice}"));
}
}
}
if let (Some(session), Some(before)) = (session, before) {
if let Some(notice) = before.record(
session,
Path::new(workspace),
&normalized,
execution.and_then(|slot| slot.get()),
) {
presentation.preview(¬ice, 0);
result.push_str(&format!("\n{notice}"));
}
}
if let Some(policy) = &policy {
if name != "run_command"
&& execution.and_then(|slot| slot.get()) == Some(&crate::ExecOutcome::Denied)
{
result.push_str(&format!("\n{}", policy.notice()));
} else if execution.and_then(|slot| slot.get()) == Some(&crate::ExecOutcome::Failed) {
result.push_str(&format!(
"\nTask worktree: {}. The original checkout remains read-only.",
task_path_literal(&policy.worktree)
));
}
}
if disposition == PromptDisposition::Act {
if let (Some(session), Some(objective)) = (session, objective) {
if let Some(hint) = crate::agentic::publish_early::after_tool(
session,
objective,
name,
args,
workspace,
execution.and_then(|slot| slot.get()).copied(),
publication.and_then(|slot| slot.get()),
&caveats.fs_read,
command_directory
.and_then(|slot| slot.get())
.map(PathBuf::as_path),
) {
result.push_str(&format!("\n{hint}"));
}
}
}
result
}
#[cfg(test)]
#[path = "../tools_tests/worktree_adoption.rs"]
mod tests;
#[cfg(test)]
#[path = "../tools_tests/worktree_adoption_round2.rs"]
mod round2_tests;
#[cfg(test)]
#[path = "../tools_tests/worktree_adoption_round3.rs"]
mod round3_tests;
#[cfg(test)]
#[path = "../tools_tests/worktree_adoption_round5.rs"]
mod round5_tests;
#[cfg(test)]
#[path = "../tools_tests/worktree_adoption_round6.rs"]
mod round6_tests;
#[cfg(test)]
#[path = "../tools_tests/worktree_adoption_fullaccess.rs"]
mod fullaccess_tests;
#[cfg(all(test, target_os = "linux"))]
#[path = "../tools_tests/worktree_adoption_refs.rs"]
mod refs_tests;
#[cfg(all(test, target_os = "linux"))]
#[path = "../tools_tests/worktree_adoption_nested.rs"]
mod nested_tests;
#[cfg(all(test, target_os = "linux"))]
#[path = "../tools_tests/worktree_adoption_sibling.rs"]
mod sibling_tests;
#[cfg(test)]
mod invalid_reference_tests {
use super::*;
#[test]
fn worktree_invalid_reference_2778_requires_matching_git_failure() {
let hint = "\nWorktree hint: To create a new branch for the worktree, use `git worktree add -b <name> <path>`.";
let failed = |stderr: &str| serde_json::json!({"exit_code":128,"stderr":stderr});
for command in [
"git worktree add ../task task",
"git -C repo worktree add '../task path' task",
] {
assert_eq!(
creation_failure_hint(command, &failed("fatal: invalid reference: task\r\n")),
Some(hint)
);
}
for (command, envelope) in [
(
"git worktree add ../task task",
failed("prefix fatal: invalid reference: task"),
),
(
"git worktree add ../task task",
failed("fatal: invalid reference: task-extra"),
),
(
"git worktree add ../task task",
failed("fatal: invalid reference: other"),
),
(
"git checkout task",
failed("fatal: invalid reference: task"),
),
(
"echo 'git worktree add ../task task'",
failed("fatal: invalid reference: task"),
),
(
"git worktree add -b task ../task missing",
failed("fatal: invalid reference: missing"),
),
(
"git worktree add ../task task",
serde_json::json!({"exit_code":0,"stderr":"fatal: invalid reference: task"}),
),
(
"git worktree add ../task task",
serde_json::json!({"exit_code":128,"stdout":"fatal: invalid reference: task","stderr":""}),
),
(
"git worktree add ../task task",
serde_json::json!({"exit_code":128,"stderr":"fatal: invalid reference: task","denied":true}),
),
(
"echo fake; git worktree add ../task task",
failed("fatal: invalid reference: task"),
),
] {
assert!(
creation_failure_hint(command, &envelope).is_none(),
"{command}: {envelope}"
);
}
}
}
#[cfg(test)]
#[path = "../tools_tests/worktree_command_cwd.rs"]
mod command_cwd_tests;
#[cfg(test)]
#[path = "../tools_tests/worktree_detect.rs"]
mod detect_tests;