git_policy_cli/worktree/worktree_remove_plan.rs
1//! What:
2//! The plan for one `git worktree remove` that the wrapper may serve with a subvolume destroy: the one directory it names.
3//! Why:
4//! Only the plain form (one directory, no option, a non-empty name) is served; `--force` and every other option change what Git checks, and a
5//! name Git resolves by its last path component is not a directory here, so those stay with Git.
6//!
7//! In TS you'd write (pseudocode):
8//! ```ts
9//! const plan = parseRemovePlan(['worktree', 'remove', '../topic'], 0, cwd); // undefined: Git removes it
10//! ```
11
12use std::ffi::OsString;
13use std::path::{Path, PathBuf};
14
15/// What:
16/// The worktree a `git worktree remove <path>` names, resolved to a directory that exists.
17/// Why:
18/// The removal needs the worktree's real path; Git also accepts a unique last component, which the wrapper leaves to
19/// Git by finding no such directory.
20///
21/// In TS you'd write (pseudocode):
22/// ```ts
23/// type RemovePlan = { target: string; operand: string };
24/// ```
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct RemovePlan {
27 /// The real path of the worktree directory.
28 pub target: PathBuf,
29 /// The operand as the person typed it, which Git also matches against the last components of every registered path.
30 pub operand: OsString,
31}
32
33/// What:
34/// Plan one invocation of `git worktree remove` that names exactly one directory and no option.
35/// Why:
36/// `--force` and every other option change what Git checks, so those forms stay with Git.
37///
38/// In TS you'd write (pseudocode):
39/// ```ts
40/// function parseRemovePlan(args: string[], subcommandIndex: number, start: string): RemovePlan | undefined;
41/// ```
42pub fn parse_remove_plan(arguments: &[OsString], subcommand_index: usize, start: &Path) -> Option<RemovePlan> {
43 if arguments.get(subcommand_index)?.as_encoded_bytes() != b"worktree" || arguments.get(subcommand_index + 1)?.as_encoded_bytes() != b"remove" {
44 return None;
45 }
46 let rest: &[OsString] = &arguments[subcommand_index + 2..];
47 let operand: &OsString = match rest {
48 [only] if !only.as_encoded_bytes().starts_with(b"-") => only,
49 [separator, only] if separator.as_encoded_bytes() == b"--" => only,
50 _ => return None,
51 };
52 // Joined to the start directory an empty operand would name that directory, while Git answers that '' is not a working tree.
53 if operand.is_empty() {
54 return None;
55 }
56 let target: PathBuf = std::fs::canonicalize(start.join(operand)).ok()?;
57 return target.is_dir().then_some(RemovePlan { target, operand: operand.clone() });
58}
59
60/// Removal plan controls stay out of the release executable.
61#[cfg(test)]
62#[path = "worktree_remove_plan_tests.rs"]
63mod tests;