git_policy_cli/rule_worktree_dirty.rs
1//! What:
2//! The question "does this worktree hold uncommitted state?" as one Git query and its reading.
3//! Why:
4//! A command that can discard uncommitted state is judged by whether any exists: a clean linked worktree has
5//! nothing to lose, a dirty one does. Tracked changes (staged or not) always count; untracked files count only
6//! for the commands that delete them.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! // git status --porcelain=v1 --untracked-files=no => dirty when it prints anything
11//! ```
12
13use super::git_metadata::MetadataOutput;
14use std::ffi::OsString;
15
16/// What:
17/// Which files count as uncommitted state.
18/// An `enum` is a closed set of named alternatives.
19/// Why:
20/// A `reset --hard` loses tracked changes only, a `clean` loses untracked files only; asking for the
21/// wrong set would reject a harmless command or let a destructive one through.
22///
23/// In TS you'd write (pseudocode):
24/// ```ts
25/// type DirtyScope = 'tracked' | 'tracked-and-untracked';
26/// ```
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum DirtyScope {
29 /// Staged and unstaged changes to tracked files.
30 Tracked,
31 /// Tracked changes and untracked files that are not ignored.
32 TrackedAndUntracked,
33}
34
35/// What:
36/// The arguments of the status query, after the invocation's global options.
37/// `Vec<OsString>` is an owned list of byte-preserving text.
38/// Why:
39/// `--no-optional-locks` keeps a read from writing the index, and the porcelain form is stable across
40/// Git versions.
41///
42/// In TS you'd write (pseudocode):
43/// ```ts
44/// const args = [...preSubcommandArgs, '--no-optional-locks', 'status', '--porcelain=v1', '--untracked-files=no'];
45/// ```
46pub fn dirty_query_arguments(global_prefix: &[OsString], scope: DirtyScope) -> Vec<OsString> {
47 // `.to_vec()` copies the borrowed prefix into a list this function owns.
48 let mut arguments: Vec<OsString> = global_prefix.to_vec();
49 for token in ["--no-optional-locks", "status", "--porcelain=v1"] {
50 arguments.push(OsString::from(token));
51 }
52 arguments.push(OsString::from(match scope {
53 DirtyScope::Tracked => "--untracked-files=no",
54 DirtyScope::TrackedAndUntracked => "--untracked-files=normal",
55 }));
56 return arguments;
57}
58
59/// What:
60/// Read the query: any output line means uncommitted state exists.
61/// `Result<bool, String>` is "the answer, or why Git could not give one".
62/// Why:
63/// A status that failed says nothing, and a safety check must not read a failure as "clean".
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// function dirtyFromOutput(output: MetadataOutput): boolean; // throws
68/// ```
69pub fn dirty_from_output(output: &MetadataOutput) -> Result<bool, String> {
70 if !output.success {
71 let detail: String = String::from_utf8_lossy(&output.stderr)
72 .lines()
73 .next()
74 .unwrap_or_default()
75 .trim()
76 .to_string();
77 return Err(format!("Git could not read the worktree status: {detail}"));
78 }
79 return Ok(!output.stdout.is_empty());
80}
81
82/// Arguments of each scope and the reading of a successful, empty, non-empty and failed status.
83#[cfg(test)]
84#[path = "rule_worktree_dirty_tests.rs"]
85mod tests;