loom_core/workspace/
reset.rs1use anyhow::Result;
2
3use crate::git::GitRepo;
4use crate::manifest::{RepoManifestEntry, WorkspaceManifest};
5
6pub struct ResetResult {
8 pub repos_reset: Vec<String>,
9 pub repos_failed: Vec<(String, String)>,
10}
11
12pub fn reset_workspace(
17 manifest: &WorkspaceManifest,
18 on_progress: impl Fn(super::ProgressEvent),
19) -> Result<ResetResult> {
20 let mut repos_reset = Vec::new();
21 let mut repos_failed = Vec::new();
22 let total = manifest.repos.len();
23
24 for (i, repo) in manifest.repos.iter().enumerate() {
25 on_progress(super::ProgressEvent::RepoStarted {
26 name: repo.name.clone(),
27 index: i,
28 total,
29 });
30
31 match reset_repo(repo, manifest.base_branch.as_deref()) {
32 Ok(()) => {
33 repos_reset.push(repo.name.clone());
34 on_progress(super::ProgressEvent::RepoComplete {
35 name: repo.name.clone(),
36 });
37 }
38 Err(e) => {
39 let msg = e.to_string();
40 repos_failed.push((repo.name.clone(), msg.clone()));
41 on_progress(super::ProgressEvent::RepoFailed {
42 name: repo.name.clone(),
43 error: msg,
44 });
45 }
46 }
47 }
48
49 Ok(ResetResult {
50 repos_reset,
51 repos_failed,
52 })
53}
54
55fn reset_repo(repo: &RepoManifestEntry, base_branch: Option<&str>) -> Result<()> {
56 let git = GitRepo::new(&repo.worktree_path);
57
58 git.reset_hard()?;
60 git.clean_untracked()?;
61
62 if let Err(e) = git.fetch() {
64 tracing::warn!(repo = %repo.name, error = %e, "fetch failed, using local state");
65 }
66
67 let branch = match base_branch {
69 Some(b) => b.to_string(),
70 None => git.default_branch().unwrap_or_else(|_| "main".to_string()),
71 };
72 let target = format!("origin/{branch}");
73
74 if git.ref_exists(&target).unwrap_or(false) {
75 if let Err(_e) = git.rebase(&target) {
77 git.rebase_abort().ok();
78 git.reset_hard_to(&target)?;
79 }
80 } else {
81 tracing::debug!(
82 repo = %repo.name,
83 target = %target,
84 "remote ref not found, skipping rebase"
85 );
86 }
87
88 Ok(())
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::manifest::RepoManifestEntry;
95
96 fn create_git_repo(path: &std::path::Path) {
97 std::fs::create_dir_all(path).unwrap();
98 std::process::Command::new("git")
99 .args(["init", "-b", "main", &path.to_string_lossy()])
100 .env("LC_ALL", "C")
101 .output()
102 .unwrap();
103 std::process::Command::new("git")
104 .args([
105 "-C",
106 &path.to_string_lossy(),
107 "commit",
108 "--allow-empty",
109 "-m",
110 "init",
111 ])
112 .env("LC_ALL", "C")
113 .output()
114 .unwrap();
115 }
116
117 #[test]
118 fn test_reset_clean_repo() {
119 let dir = tempfile::tempdir().unwrap();
120 let repo_path = dir.path().join("my-repo");
121 create_git_repo(&repo_path);
122
123 let manifest = WorkspaceManifest {
124 name: "test-ws".to_string(),
125 branch: None,
126 created: chrono::Utc::now(),
127 base_branch: None,
128 preset: None,
129 repos: vec![RepoManifestEntry {
130 name: "my-repo".to_string(),
131 original_path: repo_path.clone(),
132 worktree_path: repo_path,
133 branch: "main".to_string(),
134 remote_url: String::new(),
135 }],
136 };
137
138 let result = reset_workspace(&manifest, |_| {}).unwrap();
139 assert_eq!(result.repos_reset.len(), 1);
140 assert!(result.repos_failed.is_empty());
141 }
142
143 #[test]
144 fn test_reset_dirty_repo() {
145 let dir = tempfile::tempdir().unwrap();
146 let repo_path = dir.path().join("my-repo");
147 create_git_repo(&repo_path);
148
149 std::fs::write(repo_path.join("dirty.txt"), "content").unwrap();
151
152 let git = GitRepo::new(&repo_path);
153 assert!(git.is_dirty().unwrap());
154
155 let manifest = WorkspaceManifest {
156 name: "test-ws".to_string(),
157 branch: None,
158 created: chrono::Utc::now(),
159 base_branch: None,
160 preset: None,
161 repos: vec![RepoManifestEntry {
162 name: "my-repo".to_string(),
163 original_path: repo_path.clone(),
164 worktree_path: repo_path.clone(),
165 branch: "main".to_string(),
166 remote_url: String::new(),
167 }],
168 };
169
170 let result = reset_workspace(&manifest, |_| {}).unwrap();
171 assert_eq!(result.repos_reset.len(), 1);
172
173 assert!(!git.is_dirty().unwrap());
175 assert!(!repo_path.join("dirty.txt").exists());
176 }
177}