Skip to main content

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;