Skip to main content

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;