Skip to main content

qtcloud_devops_cli/code/
status.rs

1use std::path::PathBuf;
2
3use super::model::{ComponentStatus, StatusReport, SyncStatus};
4use crate::git::{RepoState, SubmoduleStatus};
5
6fn map_status(s: &SubmoduleStatus) -> SyncStatus {
7    match s {
8        SubmoduleStatus::Clean => SyncStatus::Synced,
9        SubmoduleStatus::AheadOfParent => SyncStatus::PendingPush,
10        SubmoduleStatus::BehindRemote => SyncStatus::PendingPull,
11        _ => SyncStatus::Conflict,
12    }
13}
14
15pub fn status(root: PathBuf, offline: bool) -> Result<StatusReport, String> {
16    let state = if offline {
17        RepoState::scan_offline(&root)
18    } else {
19        RepoState::scan(&root)
20    }
21    .map_err(|e| format!("扫描失败: {}", e))?;
22
23    let mut components = Vec::with_capacity(state.submodules.len());
24    for sm in &state.submodules {
25        let s = map_status(&sm.status);
26        components.push(ComponentStatus {
27            name: sm.name.clone(),
28            status: s,
29            ahead: sm.ahead_count,
30            behind: sm.behind_count,
31        });
32    }
33
34    let total = components.len();
35    let synced = components
36        .iter()
37        .filter(|c| c.status == SyncStatus::Synced)
38        .count();
39    let pending = total - synced;
40
41    Ok(StatusReport {
42        root: state.root_path.to_string_lossy().to_string(),
43        components,
44        total,
45        synced,
46        pending,
47    })
48}
49
50#[cfg(test)]
51mod tests {
52    fn git_init(path: &std::path::Path) {
53        let repo = git2::Repository::init(path).unwrap();
54        let mut cfg = repo.config().unwrap();
55        cfg.set_str("user.email", "t@t").unwrap();
56        cfg.set_str("user.name", "t").unwrap();
57    }
58
59    fn git_commit(path: &std::path::Path, msg: &str) {
60        std::fs::write(path.join("f"), msg).unwrap();
61        let repo = git2::Repository::open(path).unwrap();
62        let mut index = repo.index().unwrap();
63        index.add_path(std::path::Path::new("f")).unwrap();
64        index.write().unwrap();
65        let tree_id = index.write_tree().unwrap();
66        let tree = repo.find_tree(tree_id).unwrap();
67        let sig = repo.signature().unwrap();
68        let parent = repo.head().and_then(|h| h.peel_to_commit()).ok();
69        let parents: Vec<&git2::Commit> = parent.iter().collect();
70        repo.commit(Some("HEAD"), &sig, &sig, msg, &tree, &parents).unwrap();
71    }
72
73    use super::*;
74    use crate::git::SubmoduleStatus;
75
76    // ---- map_status ----
77
78    #[test]
79    fn test_map_clean() {
80        assert_eq!(map_status(&SubmoduleStatus::Clean), SyncStatus::Synced);
81    }
82    #[test]
83    fn test_map_ahead() {
84        assert_eq!(
85            map_status(&SubmoduleStatus::AheadOfParent),
86            SyncStatus::PendingPush
87        );
88    }
89    #[test]
90    fn test_map_behind() {
91        assert_eq!(
92            map_status(&SubmoduleStatus::BehindRemote),
93            SyncStatus::PendingPull
94        );
95    }
96    #[test]
97    fn test_map_detached() {
98        assert_eq!(map_status(&SubmoduleStatus::Detached), SyncStatus::Conflict);
99    }
100    #[test]
101    fn test_map_dirty() {
102        assert_eq!(map_status(&SubmoduleStatus::Dirty), SyncStatus::Conflict);
103    }
104    #[test]
105    fn test_map_orphaned() {
106        assert_eq!(map_status(&SubmoduleStatus::Orphaned), SyncStatus::Conflict);
107    }
108    #[test]
109    fn test_map_uninitialized() {
110        assert_eq!(
111            map_status(&SubmoduleStatus::Uninitialized),
112            SyncStatus::Conflict
113        );
114    }
115
116    // ---- status (integration) ----
117
118
119
120    #[test]
121    fn test_status_non_git_dir() {
122        let d = tempfile::tempdir().unwrap();
123        assert!(status(d.path().to_path_buf(), false).is_err());
124    }
125
126    #[test]
127    fn test_status_empty_repo() {
128        let d = tempfile::tempdir().unwrap();
129        git_init(d.path());
130        git_commit(d.path(), "init");
131        let report = status(d.path().to_path_buf(), false).unwrap();
132        assert_eq!(report.total, 0);
133        assert_eq!(report.synced, 0);
134        assert_eq!(report.pending, 0);
135    }
136
137    #[test]
138    fn test_status_with_synced_submodule() {
139        let tmp = tempfile::tempdir().unwrap();
140        let parent = tmp.path().join("parent");
141        let sub = tmp.path().join("sub");
142        std::fs::create_dir_all(&sub).unwrap();
143        git_init(&sub);
144        git_commit(&sub, "init sub");
145        std::fs::create_dir_all(&parent).unwrap();
146        git_init(&parent);
147        git_commit(&parent, "init parent");
148        std::process::Command::new("git")
149            .args(["submodule", "add", &sub.to_string_lossy(), "libs/sub"])
150            .current_dir(&parent)
151            .output()
152            .unwrap();
153        std::process::Command::new("git")
154            .args(["commit", "-m", "add submodule"])
155            .current_dir(&parent)
156            .output()
157            .unwrap();
158        let report = status(parent, false).unwrap();
159        assert_eq!(report.total, 1);
160        assert_eq!(report.components[0].status, SyncStatus::Synced);
161    }
162
163    #[test]
164    fn test_status_offline_flag() {
165        let d = tempfile::tempdir().unwrap();
166        git_init(d.path());
167        git_commit(d.path(), "init");
168        // offline should not prevent scanning a repo without submodules
169        assert!(status(d.path().to_path_buf(), true).is_ok());
170    }
171}