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
use std::path::Path;
use anyhow::Result;
use crate::git;
/// Tracks project worktree changes across hook priority groups.
pub(crate) struct DiffTracker<'a> {
path: &'a Path,
baseline: DiffBaseline,
}
enum DiffBaseline {
Clean,
Unknown,
Snapshot(Vec<u8>),
}
impl<'a> DiffTracker<'a> {
/// Creates a tracker for a worktree known to have no unstaged changes.
pub(crate) fn clean_baseline(path: &'a Path) -> Self {
Self {
path,
baseline: DiffBaseline::Clean,
}
}
/// Creates a tracker whose initial worktree state is unknown.
pub(crate) fn unknown_baseline(path: &'a Path) -> Self {
Self {
path,
baseline: DiffBaseline::Unknown,
}
}
/// Captures an unknown baseline before a group that requires diff tracking.
pub(crate) async fn prepare_for_group(&mut self, track_changes: bool) -> Result<()> {
if track_changes && let DiffBaseline::Unknown = self.baseline {
self.baseline = DiffBaseline::Snapshot(git::diff_worktree(self.path).await?);
}
Ok(())
}
/// Checks for worktree changes and advances the tracked baseline.
pub(crate) async fn changed_after_group(&mut self, track_changes: bool) -> Result<bool> {
if !track_changes {
return Ok(false);
}
match &mut self.baseline {
DiffBaseline::Clean => {
// `WorkTreeKeeper` already removed unstaged changes. A quiet
// worktree check keeps the common no-op path cheap.
if !git::has_worktree_diff(self.path).await? {
return Ok(false);
}
// `diff-files --quiet` is stat-based, so an in-place rewrite
// can look dirty even when the content is unchanged. Do a full
// diff here to ignore stat-only changes and reuse the content
// diff as the baseline if the hook really modified files.
let curr_diff = git::diff_worktree(self.path).await?;
if curr_diff.is_empty() {
return Ok(false);
}
// Capture the dirty state after this group so later groups can
// compare against the exact diff left by previous hooks.
self.baseline = DiffBaseline::Snapshot(curr_diff);
Ok(true)
}
DiffBaseline::Snapshot(prev_diff) => {
// Unknown initial state, `--all-files`, and later dirty groups
// need a full before/after diff comparison to avoid confusing
// pre-existing user changes with hook changes.
let curr_diff = git::diff_worktree(self.path).await?;
let modified = curr_diff != *prev_diff;
*prev_diff = curr_diff;
Ok(modified)
}
DiffBaseline::Unknown => {
unreachable!("diff baseline must be captured before hooks can modify files")
}
}
}
/// Discards the baseline after a hook reports a modification directly.
pub(crate) fn invalidate(&mut self) {
self.baseline = DiffBaseline::Unknown;
}
}