git_policy_cli/worktree/
worktree_convert_arguments.rs1pub const CONVERT_HELP: &str = "git cli-git convert-worktree-to-subvolume [--dry-run | --abandon] [--jobs <count>] (--all | <worktree>...)
13
14Convert linked worktrees that are ordinary directories into btrfs subvolumes at the same path, side by side.
15A snapshot of the main checkout then shows each converted worktree as an empty directory instead of a full copy.
16
17For each worktree the command refuses unless it is quiet: not locked, no index.lock, no operation in progress, tracked files
18unmodified, no process with its working directory or an open file inside it, and no worktree registered inside it.
19It then copies the worktree with cloned data into <worktree>.cli-git-converting, proves the copy equal, swaps the two with
20one atomic exchange, rewrites the index stat fields so the first `git status` stays fast, and removes the old tree only
21after the swap is verified. The registration and the bytes of the .git file do not change.
22
23 --all every linked worktree registered inside the main checkout
24 --dry-run list each worktree and what would happen, or why it would be skipped; change nothing
25 --abandon undo an unfinished conversion that has not reached its swap (destroys only the subvolume it created)
26 --jobs <n> convert at most n worktrees at once (also --jobs=<n>); without it the command uses the threads this process
27 may run, less a margin, and no more worktrees than the free memory holds
28
29The worktrees are named in the order given (with --all, sorted by path) and each one's lines come together, in that order,
30whichever finishes first. A worktree that fails does not stop the others: it keeps its journal, the others are converted, the
31summary counts the failures, and the status is 2. Run the same command again to finish the failed ones.
32
33An interrupted run is finished by running the same command again.
34
35Risk, accepted by running the command: it deletes the old directory once the copy is verified, and it cannot stop
36another process from changing the worktree while it works. A process that writes through an open file or a mapping, or
37renames or replaces a path, between a check and the step that relies on it can lose its data; stop such processes first.
38";
39
40pub const RISK_NOTICE: &str = "cli-git: this deletes the old directory of each worktree after the copy is verified. A process that changes a worktree while it is converted can lose data, and that risk is yours; stop such processes first.";
42
43#[derive(Clone, Debug, Default, Eq, PartialEq)]
53pub struct ConvertArguments {
54 pub dry_run: bool,
56 pub abandon: bool,
58 pub all: bool,
60 pub paths: Vec<std::ffi::OsString>,
62 pub help: bool,
64 pub command_line_attributes: bool,
66 pub jobs: Option<usize>,
68}
69
70fn parse_jobs(value: &str) -> Result<usize, String> {
72 return match value.parse::<usize>() {
73 Ok(count) if count >= 1 => Ok(count),
74 _ => Err(format!("--jobs needs a whole number of at least one, but got {value:?}")),
75 };
76}
77
78pub fn parse_convert_arguments(rest: &[std::ffi::OsString]) -> Result<ConvertArguments, String> {
88 let mut parsed: ConvertArguments = ConvertArguments::default();
89 let mut tokens = rest.iter();
90 while let Some(token) = tokens.next() {
91 let Some(word) = token.to_str() else {
92 parsed.paths.push(token.clone());
93 continue;
94 };
95 match word {
96 "--jobs" => {
97 let value: &str = tokens.next().and_then(|next: &std::ffi::OsString| { return next.to_str(); }).ok_or_else(|| { return String::from("--jobs needs a count, as in --jobs 4"); })?;
98 parsed.jobs = Some(parse_jobs(value)?);
99 }
100 other if other.starts_with("--jobs=") => parsed.jobs = Some(parse_jobs(&other["--jobs=".len()..])?),
101 "--help" | "-h" => parsed.help = true,
102 "--dry-run" => parsed.dry_run = true,
103 "--abandon" => parsed.abandon = true,
104 "--all" => parsed.all = true,
105 other if other.starts_with('-') => return Err(format!("unknown option {other}; see `git cli-git convert-worktree-to-subvolume --help`")),
106 _ => parsed.paths.push(token.clone()),
107 }
108 }
109 if parsed.dry_run && parsed.abandon {
110 return Err(String::from("--dry-run and --abandon cannot be combined"));
111 }
112 if !parsed.help && !parsed.all && parsed.paths.is_empty() {
113 return Err(String::from("name a worktree or pass --all; see `git cli-git convert-worktree-to-subvolume --help`"));
114 }
115 if parsed.all && !parsed.paths.is_empty() {
116 return Err(String::from("--all and named worktrees cannot be combined"));
117 }
118 return Ok(parsed);
119}
120
121pub fn selects_attribute_source(globals: &[std::ffi::OsString]) -> bool {
133 let mut words = globals.iter();
134 while let Some(word) = words.next() {
135 let text: String = word.to_string_lossy().to_lowercase();
136 if text.starts_with("--attr-source") || text.starts_with("--config-env=attr.tree") {
137 return true;
138 }
139 if text == "-c" && words.next().is_some_and(|value: &std::ffi::OsString| { return value.to_string_lossy().to_lowercase().starts_with("attr.tree"); }) {
140 return true;
141 }
142 }
143 return false;
144}
145
146#[cfg(test)]
148#[path = "worktree_convert_arguments_tests.rs"]
149mod tests;