Skip to main content

git_policy_cli/
auto_push_dirty_paths.rs

1//! What:
2//!  The paths a dirty worktree has uncommitted work in, the paths a fast-forward would change, and the overlap
3//!  between them.
4//! Why:
5//!  After an auto-push publishes a merge, local `main` can follow it even in a worktree that holds uncommitted
6//!  work, provided the move changes none of the files that work touches. Another session's staged renames then
7//!  never keep this branch behind the remote.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! // overlappingPaths(statusPaths(statusZ), diffPaths(diffZ))
12//! ```
13
14/// What:
15///  The paths named by `git status --porcelain=v1 -z`: each entry's path, and the original path of a rename or
16///  copy.
17///  `&[u8]` borrows Git's output; `Vec<Vec<u8>>` is an owned list of byte paths.
18/// Why:
19///   A staged rename touches both its names, and a move that changes either one would overwrite the work.
20///
21/// In TS you'd write (pseudocode):
22/// ```ts
23/// function statusPaths(output: Uint8Array): Uint8Array[];
24/// ```
25pub fn status_paths(output: &[u8]) -> Vec<Vec<u8>> {
26    let mut paths: Vec<Vec<u8>> = Vec::<Vec<u8>>::new();
27    let mut entries = output.split(|byte| { return *byte == 0; }).filter(|entry| { return !entry.is_empty(); });
28    while let Some(entry) = entries.next() {
29        // An entry is two status letters, a space, then the path.
30        if entry.len() < 4 {
31            continue;
32        }
33        paths.push(entry[3..].to_vec());
34        // A rename or copy is followed by its original path, which carries no status letters.
35        let renamed: bool = matches!(entry[0], b'R' | b'C') || matches!(entry[1], b'R' | b'C');
36        if renamed && let Some(original) = entries.next() {
37            paths.push(original.to_vec());
38        }
39    }
40    return paths;
41}
42
43/// What:
44///  The paths of `git diff --name-only -z`.
45/// Why:
46///   They are the files a fast-forward from one commit to the other writes.
47///
48/// In TS you'd write (pseudocode):
49/// ```ts
50/// function diffPaths(output: Uint8Array): Uint8Array[];
51/// ```
52pub fn diff_paths(output: &[u8]) -> Vec<Vec<u8>> {
53    return output
54        .split(|byte| { return *byte == 0; })
55        .filter(|path| { return !path.is_empty(); })
56        .map(<[u8]>::to_vec)
57        .collect();
58}
59
60/// What:
61///  The dirty paths a move would also change, without repeats.
62/// Why:
63///   Exactly these paths decide whether the move may run.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// function overlappingPaths(dirty: Uint8Array[], incoming: Uint8Array[]): Uint8Array[];
68/// ```
69pub fn overlapping_paths(dirty: &[Vec<u8>], incoming: &[Vec<u8>]) -> Vec<Vec<u8>> {
70    let mut overlap: Vec<Vec<u8>> = Vec::<Vec<u8>>::new();
71    for path in dirty {
72        if incoming.contains(path) && !overlap.contains(path) {
73            overlap.push(path.clone());
74        }
75    }
76    return overlap;
77}
78
79/// The most paths a report names before it counts the rest.
80const LISTED_LIMIT: usize = 5;
81
82/// What:
83///  The overlapping paths as text for the report: the first few by name, then a count.
84/// Why:
85///   The person must see which files held the branch back, and a report is not a place for a hundred names.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// function listed(paths: Uint8Array[]): string;
90/// ```
91pub fn listed(paths: &[Vec<u8>]) -> String {
92    let mut names: Vec<String> = paths
93        .iter()
94        .take(LISTED_LIMIT)
95        .map(|path| { return String::from_utf8_lossy(path).into_owned(); })
96        .collect();
97    if paths.len() > LISTED_LIMIT {
98        names.push(format!("and {} more", paths.len() - LISTED_LIMIT));
99    }
100    return names.join(", ");
101}
102
103/// Status entries, renames, copies, the overlap and the report text.
104#[cfg(test)]
105#[path = "auto_push_dirty_paths_tests.rs"]
106mod tests;