Skip to main content

qtcloud_devops_cli/git/
types.rs

1pub use std::path::PathBuf;
2
3/// 截断 OID 到 7 字符显示(等价旧 CommitHash Display)。
4pub fn fmt_oid(id: &gix::ObjectId) -> String {
5    gix::hash::Prefix::new(id, 7)
6        .map(|p| p.to_string())
7        .unwrap_or_else(|_| id.to_string())
8}
9
10#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
11pub enum SubmoduleStatus {
12    Clean,
13    AheadOfParent,
14    BehindRemote,
15    Detached,
16    Dirty,
17    Orphaned,
18    Uninitialized,
19}
20
21impl SubmoduleStatus {
22    pub fn priority(&self) -> u8 {
23        match self {
24            Self::Dirty => 0,
25            Self::Orphaned => 1,
26            Self::Detached => 2,
27            Self::Uninitialized => 3,
28            Self::BehindRemote => 4,
29            Self::AheadOfParent => 5,
30            Self::Clean => 6,
31        }
32    }
33}
34
35#[derive(Debug, Clone, serde::Serialize)]
36pub struct Submodule {
37    pub name: String,
38    pub path: PathBuf,
39    pub url: String,
40    pub tracked_branch: String,
41    pub parent_pointer: gix::ObjectId,
42    pub local_head: gix::ObjectId,
43    pub remote_head: gix::ObjectId,
44    pub status: SubmoduleStatus,
45    pub ahead_count: usize,
46    pub behind_count: usize,
47    pub remote_unreachable: bool,
48}
49
50#[derive(Debug, Clone, serde::Serialize)]
51pub struct RepoState {
52    pub root_path: PathBuf,
53    pub submodules: Vec<Submodule>,
54    pub total: usize,
55    pub clean_count: usize,
56    pub needs_attention: Vec<String>,
57}
58
59#[derive(Debug, Clone, Default, serde::Serialize)]
60pub struct AggregateStatus {
61    pub total: usize,
62    pub clean: usize,
63    pub ahead_of_parent: usize,
64    pub behind_remote: usize,
65    pub detached: usize,
66    pub dirty: usize,
67    pub orphaned: usize,
68    pub uninitialized: usize,
69}
70
71impl AggregateStatus {
72    pub fn from_submodules(submodules: &[Submodule]) -> Self {
73        let mut clean = 0;
74        let mut ahead = 0;
75        let mut behind = 0;
76        let mut detached = 0;
77        let mut dirty = 0;
78        let mut orphaned = 0;
79        let mut uninit = 0;
80        for sm in submodules {
81            match sm.status {
82                SubmoduleStatus::Clean => clean += 1,
83                SubmoduleStatus::AheadOfParent => ahead += 1,
84                SubmoduleStatus::BehindRemote => behind += 1,
85                SubmoduleStatus::Detached => detached += 1,
86                SubmoduleStatus::Dirty => dirty += 1,
87                SubmoduleStatus::Orphaned => orphaned += 1,
88                SubmoduleStatus::Uninitialized => uninit += 1,
89            }
90        }
91        AggregateStatus {
92            total: submodules.len(),
93            clean,
94            ahead_of_parent: ahead,
95            behind_remote: behind,
96            detached,
97            dirty,
98            orphaned,
99            uninitialized: uninit,
100        }
101    }
102}
103
104#[derive(Debug, Clone)]
105pub struct HealthIssue {
106    pub submodule_name: String,
107    pub status: String,
108    pub description: String,
109    pub suggested_action: String,
110}
111
112pub fn describe_issue(status: &SubmoduleStatus) -> (String, String) {
113    match status {
114        SubmoduleStatus::AheadOfParent => (
115            "本地领先于父仓库记录".into(),
116            "运行 sync_to_parent 更新父仓库指针".into(),
117        ),
118        SubmoduleStatus::BehindRemote => (
119            "远程有更新,本地落后".into(),
120            "运行 code sync 获取最新代码".into(),
121        ),
122        SubmoduleStatus::Detached => (
123            "处于游离 HEAD 状态".into(),
124            "运行 checkout_branch 切换到跟踪分支".into(),
125        ),
126        SubmoduleStatus::Dirty => ("有未提交的修改".into(), "提交或 stash 当前修改".into()),
127        SubmoduleStatus::Orphaned => (
128            "父仓库记录的 commit 在远程已不存在".into(),
129            "需手动干预".into(),
130        ),
131        SubmoduleStatus::Uninitialized => ("尚未初始化".into(), "运行 init 初始化子模块".into()),
132        SubmoduleStatus::Clean => unreachable!(),
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    // ---- SubmoduleStatus tests ----
141    #[test]
142    fn test_status_priority_ordering() {
143        assert!(SubmoduleStatus::Dirty.priority() < SubmoduleStatus::Clean.priority());
144        assert!(SubmoduleStatus::Orphaned.priority() < SubmoduleStatus::BehindRemote.priority());
145    }
146    #[test]
147    fn test_clean_is_lowest_priority() {
148        for s in &[
149            SubmoduleStatus::Dirty,
150            SubmoduleStatus::Orphaned,
151            SubmoduleStatus::Detached,
152            SubmoduleStatus::Uninitialized,
153            SubmoduleStatus::BehindRemote,
154            SubmoduleStatus::AheadOfParent,
155        ] {
156            assert!(s.priority() < SubmoduleStatus::Clean.priority());
157        }
158    }
159    #[test]
160    fn test_all_priorities_are_unique() {
161        let p: Vec<u8> = [
162            SubmoduleStatus::Dirty,
163            SubmoduleStatus::Orphaned,
164            SubmoduleStatus::Detached,
165            SubmoduleStatus::Uninitialized,
166            SubmoduleStatus::BehindRemote,
167            SubmoduleStatus::AheadOfParent,
168            SubmoduleStatus::Clean,
169        ]
170        .iter()
171        .map(|s| s.priority())
172        .collect();
173        let mut s = p.clone();
174        s.sort();
175        s.dedup();
176        assert_eq!(p.len(), s.len());
177    }
178    #[test]
179    fn test_status_debug_output() {
180        assert_eq!(format!("{:?}", SubmoduleStatus::Clean), "Clean");
181    }
182    #[test]
183    fn test_status_clone_eq() {
184        assert_eq!(SubmoduleStatus::Dirty, SubmoduleStatus::Dirty);
185    }
186
187    // ---- fmt_oid ----
188    #[test]
189    fn test_fmt_oid_truncates() {
190        let oid = gix::ObjectId::from_hex(b"abcdef1234567890abcdef1234567890abcdef12").unwrap();
191        assert_eq!(fmt_oid(&oid), "abcdef1");
192    }
193    #[test]
194    fn test_fmt_oid_short() {
195        // 创建一个只有 3 有效 hex 位的 OID 不好搞,直接测全零
196        let oid = gix::ObjectId::null(gix::hash::Kind::Sha1);
197        assert_eq!(fmt_oid(&oid).len(), 7);
198    }
199    #[test]
200    fn test_fmt_oid_null() {
201        let oid = gix::ObjectId::null(gix::hash::Kind::Sha1);
202        let s = fmt_oid(&oid);
203        assert_eq!(s.len(), 7);
204        assert!(s.chars().all(|c| c == '0'));
205    }
206
207    // ---- Submodule ----
208    #[test]
209    fn test_submodule_builder() {
210        let sm = Submodule {
211            name: "test".into(),
212            path: PathBuf::from("libs/test"),
213            url: "https://example.com/test.git".into(),
214            tracked_branch: "main".into(),
215            parent_pointer: gix::ObjectId::null(gix::hash::Kind::Sha1),
216            local_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
217            remote_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
218            status: SubmoduleStatus::BehindRemote,
219            ahead_count: 0,
220            behind_count: 3,
221            remote_unreachable: false,
222        };
223        assert_eq!(sm.name, "test");
224    }
225
226    // ---- AggregateStatus ----
227    #[test]
228    fn test_aggregate_status_default() {
229        assert_eq!(AggregateStatus::default().total, 0);
230    }
231    #[test]
232    fn test_aggregate_status_from_submodules() {
233        let sm = |s| Submodule {
234            name: String::new(),
235            path: PathBuf::new(),
236            url: String::new(),
237            tracked_branch: "main".into(),
238            parent_pointer: gix::ObjectId::null(gix::hash::Kind::Sha1),
239            local_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
240            remote_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
241            status: s,
242            ahead_count: 0,
243            behind_count: 0,
244            remote_unreachable: false,
245        };
246        let agg = AggregateStatus::from_submodules(&[
247            sm(SubmoduleStatus::Clean),
248            sm(SubmoduleStatus::Dirty),
249            sm(SubmoduleStatus::Orphaned),
250        ]);
251        assert_eq!(agg.total, 3);
252        assert_eq!(agg.clean, 1);
253        assert_eq!(agg.dirty, 1);
254        assert_eq!(agg.orphaned, 1);
255    }
256    #[test]
257    fn test_aggregate_status_all_variants() {
258        let sm = |s| Submodule {
259            name: String::new(),
260            path: PathBuf::new(),
261            url: String::new(),
262            tracked_branch: "main".into(),
263            parent_pointer: gix::ObjectId::null(gix::hash::Kind::Sha1),
264            local_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
265            remote_head: gix::ObjectId::null(gix::hash::Kind::Sha1),
266            status: s,
267            ahead_count: 0,
268            behind_count: 0,
269            remote_unreachable: false,
270        };
271        let agg = AggregateStatus::from_submodules(&[
272            sm(SubmoduleStatus::Clean),
273            sm(SubmoduleStatus::AheadOfParent),
274            sm(SubmoduleStatus::BehindRemote),
275            sm(SubmoduleStatus::Detached),
276            sm(SubmoduleStatus::Dirty),
277            sm(SubmoduleStatus::Orphaned),
278            sm(SubmoduleStatus::Uninitialized),
279        ]);
280        assert_eq!(agg.total, 7);
281        assert_eq!(agg.clean, 1);
282    }
283
284    // ---- RepoState ----
285    #[test]
286    fn test_repo_state_empty() {
287        let s = RepoState {
288            root_path: PathBuf::from("/tmp"),
289            submodules: vec![],
290            total: 0,
291            clean_count: 0,
292            needs_attention: vec![],
293        };
294        assert_eq!(s.total, 0);
295    }
296
297    // ---- describe_issue ----
298    #[test]
299    fn test_describe_issue_ahead_of_parent() {
300        let (d, a) = describe_issue(&SubmoduleStatus::AheadOfParent);
301        assert!(d.contains("领先"));
302        assert!(a.contains("sync"));
303    }
304    #[test]
305    fn test_describe_issue_behind_remote() {
306        let (d, a) = describe_issue(&SubmoduleStatus::BehindRemote);
307        assert!(d.contains("落后"));
308        assert!(a.contains("sync"));
309    }
310    #[test]
311    fn test_describe_issue_detached() {
312        let (d, a) = describe_issue(&SubmoduleStatus::Detached);
313        assert!(d.contains("游离"));
314        assert!(a.contains("checkout"));
315    }
316    #[test]
317    fn test_describe_issue_dirty() {
318        let (d, _a) = describe_issue(&SubmoduleStatus::Dirty);
319        assert!(d.contains("修改"));
320    }
321    #[test]
322    fn test_describe_issue_orphaned() {
323        let (d, _a) = describe_issue(&SubmoduleStatus::Orphaned);
324        assert!(d.contains("不存在"));
325    }
326    #[test]
327    fn test_describe_issue_uninitialized() {
328        let (d, _a) = describe_issue(&SubmoduleStatus::Uninitialized);
329        assert!(d.contains("初始化"));
330    }
331    #[test]
332    #[should_panic(expected = "unreachable")]
333    fn test_describe_issue_clean_panics() {
334        describe_issue(&SubmoduleStatus::Clean);
335    }
336}