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