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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
//! In-process git status capture via `gix`.
//!
//! Used by `shoka cache refresh` to populate the per-repo status
//! snapshot the TUI displays as columns (branch / ahead-behind /
//! dirty). All work is in-process — no `git status` subprocess fan-
//! out, which matters most on Windows where `CreateProcess` dominates
//! the cost over a few-hundred-repo shelf.
//!
//! What's captured (Phase 2a):
//!
//! - `branch` — HEAD's shorthand name, or `HEAD` for a detached
//! head. `None` when the repo has no commits yet (just-cloned, or
//! an unborn branch).
//! - `dirty` — `true` when the working tree has any modifications
//! (tracked or untracked) relative to HEAD. The TUI uses a single
//! ●/✓ glyph rather than per-status counts; the bool is what we
//! actually need.
//! - `ahead` / `behind` — commit counts of HEAD relative to its
//! upstream (`origin/<branch>` by convention). `None` when there
//! is no remote ref to compare against (offline, fresh clone with
//! no fetch yet, …).
//!
//! Phase 2b adds GitHub PR / CI counts via `octocrab` on top of this.
use Path;
use Result;
use ;
/// Captured snapshot of a repo's local git state. Stored on the
/// per-repo cache entry; serialised verbatim into `cache.toml`.
/// Capture the status snapshot for the repo rooted at `repo_root`.
///
/// Errors propagate when the path can't be opened as a git repo
/// (caller treats them as "skip this entry, leave the previous
/// snapshot intact"). A successful capture with `branch = None`
/// means the repo is valid but has no commits yet.
/// Read HEAD's shorthand. Returns `None` when the repo has no
/// commits yet (head() errs with an unborn-branch case); returns
/// `"HEAD"` literally for a detached HEAD so the TUI can render
/// "you're not on a branch" visibly rather than as a blank cell.
/// Best-effort working-tree dirtiness for the repo at `repo_root`.
///
/// A focused entry point for callers (e.g. `shoka rm`'s pre-delete
/// safety gate) that need only the clean/dirty bit, not the full
/// [`capture`] snapshot with its ahead/behind walk. Mirrors
/// [`capture`]'s `dirty` semantics exactly — it shares the same
/// [`is_dirty`] core — so the two never drift. Errors propagate so the
/// caller can decide how cautious to be when the repo can't be read.
/// Is the working tree dirty? Errors are treated as "couldn't
/// tell" — gix's status path can fail on weird repo states (broken
/// symlinks, permission issues on a single file) and we'd rather
/// surface a clean snapshot with `dirty = false` than abort the
/// whole refresh because of one repo.
/// Count commits HEAD has vs. its upstream, and vice versa. Both
/// sides `None` when there's no upstream ref (no fetch yet, or no
/// remote configured); both `Some(0)` when HEAD == upstream.
///
/// Convention: upstream is `refs/remotes/origin/<branch>`. shoka's
/// scope doesn't yet track per-branch upstreams (the `branch.<name>
/// .remote` config gix could read) — `origin` covers the
/// overwhelming majority of yukimemi/* setups, and the right answer
/// for "what should I show by default" is the same one most TUIs
/// pick. Real per-branch upstream resolution is a Phase 2b polish.
/// Count commits reachable from `tip` but **not** from `hide`.
/// Equivalent to `git rev-list --count <tip> ^<hide>`.
///
/// Uses gix's [`with_hidden`] so the walker treats `hide` *and its
/// entire ancestor set* as off-limits. Without that, a diverged
/// history (merge commits, side branches) would walk back to the
/// root commit chasing parents that don't share an obvious linear
/// path; performance scales with diverge distance instead of total
/// repo history.
///
/// [`with_hidden`]: gix::revision::walk::Platform::with_hidden