quanttide_devops/source/git/
submodule.rs1pub fn fmt_oid(id: &gix::ObjectId) -> String {
7 gix::hash::Prefix::new(id, 7)
8 .map(|p| p.to_string())
9 .unwrap_or_else(|_| id.to_string())
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
14pub enum SubmoduleStatus {
15 Clean,
16 AheadOfParent,
17 BehindRemote,
18 Detached,
19 Dirty,
20 Orphaned,
21 Uninitialized,
22}
23
24impl SubmoduleStatus {
25 pub fn priority(&self) -> u8 {
27 match self {
28 Self::Dirty => 0,
29 Self::Orphaned => 1,
30 Self::Detached => 2,
31 Self::Uninitialized => 3,
32 Self::BehindRemote => 4,
33 Self::AheadOfParent => 5,
34 Self::Clean => 6,
35 }
36 }
37}
38
39#[derive(Debug, Clone, serde::Serialize)]
41pub struct Submodule {
42 pub name: String,
43 pub path: std::path::PathBuf,
44 pub url: String,
45 pub tracked_branch: String,
46 pub parent_pointer: gix::ObjectId,
47 pub local_head: gix::ObjectId,
48 pub remote_head: gix::ObjectId,
49 pub status: SubmoduleStatus,
50 pub ahead_count: usize,
51 pub behind_count: usize,
52 pub remote_unreachable: bool,
53}
54
55#[derive(Debug, Clone, serde::Serialize)]
57pub struct RepoState {
58 pub root_path: std::path::PathBuf,
59 pub submodules: Vec<Submodule>,
60 pub total: usize,
61 pub clean_count: usize,
62 pub needs_attention: Vec<String>,
63}
64
65#[derive(Debug, Clone, Default, serde::Serialize)]
67pub struct AggregateStatus {
68 pub total: usize,
69 pub clean: usize,
70 pub ahead_of_parent: usize,
71 pub behind_remote: usize,
72 pub detached: usize,
73 pub dirty: usize,
74 pub orphaned: usize,
75 pub uninitialized: usize,
76}
77
78impl AggregateStatus {
79 pub fn from_submodules(submodules: &[Submodule]) -> Self {
81 let mut clean = 0;
82 let mut ahead = 0;
83 let mut behind = 0;
84 let mut detached = 0;
85 let mut dirty = 0;
86 let mut orphaned = 0;
87 let mut uninit = 0;
88 for sm in submodules {
89 match sm.status {
90 SubmoduleStatus::Clean => clean += 1,
91 SubmoduleStatus::AheadOfParent => ahead += 1,
92 SubmoduleStatus::BehindRemote => behind += 1,
93 SubmoduleStatus::Detached => detached += 1,
94 SubmoduleStatus::Dirty => dirty += 1,
95 SubmoduleStatus::Orphaned => orphaned += 1,
96 SubmoduleStatus::Uninitialized => uninit += 1,
97 }
98 }
99 AggregateStatus {
100 total: submodules.len(),
101 clean,
102 ahead_of_parent: ahead,
103 behind_remote: behind,
104 detached,
105 dirty,
106 orphaned,
107 uninitialized: uninit,
108 }
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct HealthIssue {
115 pub submodule_name: String,
116 pub status: String,
117 pub description: String,
118 pub suggested_action: String,
119}
120
121pub fn describe_issue(status: &SubmoduleStatus) -> (String, String) {
123 match status {
124 SubmoduleStatus::AheadOfParent => (
125 "本地领先于父仓库记录".into(),
126 "运行 sync_to_parent 更新父仓库指针".into(),
127 ),
128 SubmoduleStatus::BehindRemote => (
129 "远程有更新,本地落后".into(),
130 "运行 code sync 获取最新代码".into(),
131 ),
132 SubmoduleStatus::Detached => (
133 "处于游离 HEAD 状态".into(),
134 "运行 checkout_branch 切换到跟踪分支".into(),
135 ),
136 SubmoduleStatus::Dirty => ("有未提交的修改".into(), "提交或 stash 当前修改".into()),
137 SubmoduleStatus::Orphaned => (
138 "父仓库记录的 commit 在远程已不存在".into(),
139 "需手动干预".into(),
140 ),
141 SubmoduleStatus::Uninitialized => ("尚未初始化".into(), "运行 init 初始化子模块".into()),
142 SubmoduleStatus::Clean => unreachable!(),
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_fmt_oid_null() {
152 let null = gix::ObjectId::null(gix::hash::Kind::Sha1);
153 assert_eq!(fmt_oid(&null).len(), 7);
154 }
155
156 #[test]
157 fn test_submodule_status_priority() {
158 assert!(SubmoduleStatus::Dirty.priority() < SubmoduleStatus::Clean.priority());
159 }
160
161 #[test]
162 fn test_aggregate_from_submodules() {
163 let sm = |s: SubmoduleStatus| Submodule {
164 name: "test".into(),
165 path: "test".into(),
166 url: "".into(),
167 tracked_branch: "main".into(),
168 parent_pointer: gix::ObjectId::null(gix::hash::Kind::Sha1),
169 local_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
170 remote_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
171 status: s,
172 ahead_count: 0,
173 behind_count: 0,
174 remote_unreachable: false,
175 };
176 let subs = vec![sm(SubmoduleStatus::Clean), sm(SubmoduleStatus::Dirty)];
177 let agg = AggregateStatus::from_submodules(&subs);
178 assert_eq!(agg.total, 2);
179 assert_eq!(agg.clean, 1);
180 assert_eq!(agg.dirty, 1);
181 }
182
183 #[test]
184 fn test_aggregate_from_submodules_all_variants() {
185 let sm = |s: SubmoduleStatus| Submodule {
186 name: "test".into(),
187 path: "test".into(),
188 url: "".into(),
189 tracked_branch: "main".into(),
190 parent_pointer: gix::ObjectId::null(gix::hash::Kind::Sha1),
191 local_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
192 remote_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
193 status: s,
194 ahead_count: 0,
195 behind_count: 0,
196 remote_unreachable: false,
197 };
198 let subs = vec![
199 sm(SubmoduleStatus::Clean),
200 sm(SubmoduleStatus::AheadOfParent),
201 sm(SubmoduleStatus::BehindRemote),
202 sm(SubmoduleStatus::Detached),
203 sm(SubmoduleStatus::Dirty),
204 sm(SubmoduleStatus::Orphaned),
205 sm(SubmoduleStatus::Uninitialized),
206 ];
207 let agg = AggregateStatus::from_submodules(&subs);
208 assert_eq!(agg.total, 7);
209 assert_eq!(agg.clean, 1);
210 assert_eq!(agg.ahead_of_parent, 1);
211 assert_eq!(agg.behind_remote, 1);
212 assert_eq!(agg.detached, 1);
213 assert_eq!(agg.dirty, 1);
214 assert_eq!(agg.orphaned, 1);
215 assert_eq!(agg.uninitialized, 1);
216 }
217
218 #[test]
219 fn test_describe_issue_all_variants() {
220 for variant in &[
221 SubmoduleStatus::AheadOfParent,
222 SubmoduleStatus::BehindRemote, SubmoduleStatus::Detached,
223 SubmoduleStatus::Dirty, SubmoduleStatus::Orphaned,
224 SubmoduleStatus::Uninitialized,
225 ] {
226 let (desc, action) = describe_issue(variant);
227 assert!(!desc.is_empty());
228 assert!(!action.is_empty());
229 }
230 }
231
232 #[test]
233 #[should_panic(expected = "internal error: entered unreachable code")]
234 fn test_describe_issue_clean_panics() {
235 describe_issue(&SubmoduleStatus::Clean);
236 }
237
238 #[test]
239 fn test_all_priorities_are_unique() {
240 let mut prios: Vec<u8> = vec![
241 SubmoduleStatus::Clean.priority(),
242 SubmoduleStatus::AheadOfParent.priority(),
243 SubmoduleStatus::BehindRemote.priority(),
244 SubmoduleStatus::Detached.priority(),
245 SubmoduleStatus::Dirty.priority(),
246 SubmoduleStatus::Orphaned.priority(),
247 SubmoduleStatus::Uninitialized.priority(),
248 ];
249 prios.sort();
250 prios.dedup();
251 assert_eq!(prios.len(), 7);
252 }
253
254 #[test]
255 fn test_clean_is_lowest_priority() {
256 let prios = [
257 SubmoduleStatus::Clean, SubmoduleStatus::AheadOfParent,
258 SubmoduleStatus::BehindRemote, SubmoduleStatus::Detached,
259 SubmoduleStatus::Dirty, SubmoduleStatus::Orphaned,
260 SubmoduleStatus::Uninitialized,
261 ];
262 let max = prios.iter().map(|s| s.priority()).max().unwrap();
263 assert_eq!(max, SubmoduleStatus::Clean.priority());
264 }
265}