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