1use std::fs;
2use std::path::{Path, PathBuf};
3
4use super::config;
5use super::output::*;
6use super::path;
7use super::repo;
8use super::worktree;
9
10pub struct Tree {
11 pub root: String,
12 pub repos: Vec<repo::Repo>,
13}
14
15pub fn find_unmanaged_repos(
16 root_path: &Path,
17 managed_repos: &[repo::Repo],
18) -> Result<Vec<PathBuf>, String> {
19 let mut unmanaged_repos = Vec::new();
20
21 for repo_path in find_repo_paths(root_path)? {
22 if !managed_repos
23 .iter()
24 .any(|r| Path::new(root_path).join(r.fullname()) == repo_path)
25 {
26 unmanaged_repos.push(repo_path);
27 }
28 }
29 Ok(unmanaged_repos)
30}
31
32pub fn sync_trees(config: config::Config, init_worktree: bool) -> Result<bool, String> {
33 let mut failures = false;
34
35 let mut unmanaged_repos_absolute_paths = vec![];
36 let mut managed_repos_absolute_paths = vec![];
37
38 let trees = config.trees()?;
39
40 for tree in trees {
41 let repos: Vec<repo::Repo> = tree
42 .repos
43 .unwrap_or_default()
44 .into_iter()
45 .map(|repo| repo.into_repo())
46 .collect();
47
48 let root_path = path::expand_path(Path::new(&tree.root));
49
50 for repo in &repos {
51 managed_repos_absolute_paths.push(root_path.join(repo.fullname()));
52 match sync_repo(&root_path, repo, init_worktree) {
53 Ok(()) => print_repo_success(&repo.name, "OK"),
54 Err(error) => {
55 print_repo_error(&repo.name, &error);
56 failures = true;
57 }
58 }
59 }
60
61 match find_unmanaged_repos(&root_path, &repos) {
62 Ok(repos) => {
63 for path in repos {
64 if !unmanaged_repos_absolute_paths.contains(&path) {
65 unmanaged_repos_absolute_paths.push(path);
66 }
67 }
68 }
69 Err(error) => {
70 print_error(&format!("Error getting unmanaged repos: {error}"));
71 failures = true;
72 }
73 }
74 }
75
76 for unmanaged_repo_absolute_path in &unmanaged_repos_absolute_paths {
77 if managed_repos_absolute_paths
78 .iter()
79 .any(|managed_repo_absolute_path| {
80 managed_repo_absolute_path == unmanaged_repo_absolute_path
81 })
82 {
83 continue;
84 }
85 print_warning(&format!(
86 "Found unmanaged repository: \"{}\"",
87 path::path_as_string(unmanaged_repo_absolute_path)
88 ));
89 }
90
91 Ok(!failures)
92}
93
94pub fn find_repo_paths(path: &Path) -> Result<Vec<PathBuf>, String> {
96 let mut repos = Vec::new();
97
98 let git_dir = path.join(".git");
99 let git_worktree = path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY);
100
101 if git_dir.exists() || git_worktree.exists() {
102 repos.push(path.to_path_buf());
103 } else {
104 match fs::read_dir(path) {
105 Ok(contents) => {
106 for content in contents {
107 match content {
108 Ok(entry) => {
109 let path = entry.path();
110 if path.is_symlink() {
111 continue;
112 }
113 if path.is_dir() {
114 match find_repo_paths(&path) {
115 Ok(ref mut r) => repos.append(r),
116 Err(error) => return Err(error),
117 }
118 }
119 }
120 Err(error) => {
121 return Err(format!("Error accessing directory: {error}"));
122 }
123 };
124 }
125 }
126 Err(e) => {
127 return Err(format!(
128 "Failed to open \"{}\": {}",
129 &path.display(),
130 match e.kind() {
131 std::io::ErrorKind::NotFound => String::from("not found"),
132 _ => format!("{:?}", e.kind()),
133 }
134 ));
135 }
136 };
137 }
138
139 Ok(repos)
140}
141
142fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result<(), String> {
143 let repo_path = root_path.join(repo.fullname());
144 let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
145
146 let mut newly_created = false;
147
148 if repo_path.exists()
171 && repo_path
172 .read_dir()
173 .map_err(|error| error.to_string())?
174 .next()
175 .is_some()
176 {
177 if repo.worktree_setup && !actual_git_directory.exists() {
178 return Err(String::from(
179 "Repo already exists, but is not using a worktree setup",
180 ));
181 };
182 } else if repo.remotes.is_none() || repo.remotes.as_ref().unwrap().is_empty() {
183 print_repo_action(
184 &repo.name,
185 "Repository does not have remotes configured, initializing new",
186 );
187 match repo::RepoHandle::init(&repo_path, repo.worktree_setup) {
188 Ok(r) => {
189 print_repo_success(&repo.name, "Repository created");
190 Some(r)
191 }
192 Err(error) => {
193 return Err(format!("Repository failed during init: {error}"));
194 }
195 };
196 } else {
197 let first = repo.remotes.as_ref().unwrap().first().unwrap();
198
199 match repo::clone_repo(first, &repo_path, repo.worktree_setup) {
200 Ok(()) => {
201 print_repo_success(&repo.name, "Repository successfully cloned");
202 }
203 Err(error) => {
204 return Err(format!("Repository failed during clone: {error}"));
205 }
206 };
207
208 newly_created = true;
209 }
210
211 let repo_handle = match repo::RepoHandle::open(&repo_path, repo.worktree_setup) {
212 Ok(repo) => repo,
213 Err(error) => {
214 return if !repo.worktree_setup && repo::RepoHandle::open(&repo_path, true).is_ok() {
215 Err(String::from(
216 "Repo already exists, but is using a worktree setup",
217 ))
218 } else {
219 Err(format!("Opening repository failed: {error}"))
220 }
221 }
222 };
223
224 if newly_created && repo.worktree_setup && init_worktree {
225 match repo_handle.default_branch() {
226 Ok(branch) => {
227 worktree::add_worktree(&repo_path, &branch.name()?, None, false)?;
228 }
229 Err(_error) => print_repo_error(
230 &repo.name,
231 "Could not determine default branch, skipping worktree initializtion",
232 ),
233 }
234 }
235 if let Some(remotes) = &repo.remotes {
236 let current_remotes: Vec<String> = repo_handle
237 .remotes()
238 .map_err(|error| format!("Repository failed during getting the remotes: {error}"))?;
239
240 for remote in remotes {
241 let current_remote = repo_handle.find_remote(&remote.name)?;
242
243 match current_remote {
244 Some(current_remote) => {
245 let current_url = current_remote.url();
246
247 if remote.url != current_url {
248 print_repo_action(
249 &repo.name,
250 &format!("Updating remote {} to \"{}\"", &remote.name, &remote.url),
251 );
252 if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) {
253 return Err(format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e));
254 };
255 }
256 }
257 None => {
258 print_repo_action(
259 &repo.name,
260 &format!(
261 "Setting up new remote \"{}\" to \"{}\"",
262 &remote.name, &remote.url
263 ),
264 );
265 if let Err(error) = repo_handle.new_remote(&remote.name, &remote.url) {
266 return Err(format!(
267 "Repository failed during setting the remotes: {error}",
268 ));
269 }
270 }
271 }
272 }
273
274 for current_remote in ¤t_remotes {
275 if !remotes.iter().any(|r| &r.name == current_remote) {
276 print_repo_action(
277 &repo.name,
278 &format!("Deleting remote \"{}\"", ¤t_remote,),
279 );
280 if let Err(e) = repo_handle.remote_delete(current_remote) {
281 return Err(format!(
282 "Repository failed during deleting remote \"{}\": {}",
283 ¤t_remote, e
284 ));
285 }
286 }
287 }
288 }
289 Ok(())
290}
291
292fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf {
293 if is_worktree {
294 path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY)
295 } else {
296 path.to_path_buf()
297 }
298}