Skip to main content

quanttide_devops/source/git/
submodule.rs

1//! 子模块数据模型。
2//!
3//! 提供子模块的枚举、结构体等纯数据模型。I/O 操作在 CLI 层。
4
5/// 截断 OID 到 7 字符显示。
6pub 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/// 子模块同步状态。
13#[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    /// 优先级(数值越小越需要关注)。
26    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/// 子模块信息。
40#[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/// 仓库子模块快照。
56#[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/// 子模块状态聚合统计。
66#[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    /// 从子模块列表聚合统计。
80    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/// 子模块健康问题描述。
113#[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
121/// 根据子模块状态返回(问题描述,建议操作)。
122pub 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}