1use std::{
5 fs,
6 path::{Path, PathBuf},
7};
8
9use thiserror::Error;
10
11use super::{
12 config,
13 output::{print_error, print_repo_action, print_repo_error, print_repo_success, print_warning},
14 path, repo,
15 worktree::{self, WorktreeName},
16};
17
18#[derive(Debug, Error)]
19pub enum Error {
20 #[error(transparent)]
21 Config(#[from] config::Error),
22 #[error(transparent)]
23 Repo(#[from] repo::Error),
24 #[error(transparent)]
25 Worktree(#[from] worktree::Error),
26 #[error("Failed to open \"{:?}\": Not found", .path)]
27 NotFound { path: PathBuf },
28 #[error("Failed to open \"{:?}\": {}", .path, .kind)]
29 Open {
30 path: PathBuf,
31 kind: std::io::ErrorKind,
32 },
33 #[error(transparent)]
34 Io(#[from] std::io::Error),
35 #[error("Error accessing directory: {}", .message)]
36 DirectoryAccess { message: String },
37 #[error("Repo already exists, but is not using a worktree setup")]
38 WorktreeExpected,
39 #[error("Repo already exists, but is using a worktree setup")]
40 WorktreeNotExpected,
41 #[error("Repository failed during init: {}", .message)]
42 InitFailed { message: String },
43 #[error("Repository failed during clone: {}", .message)]
44 CloneFailed { message: String },
45 #[error(transparent)]
46 Path(#[from] path::Error),
47}
48
49#[derive(Debug)]
50pub struct Root(PathBuf);
51
52impl Root {
53 pub fn new(s: PathBuf) -> Self {
54 Self(s)
55 }
56
57 pub fn as_path(&self) -> &Path {
58 &self.0
59 }
60
61 pub fn into_path_buf(self) -> PathBuf {
62 self.0
63 }
64}
65
66impl From<config::Root> for Root {
67 fn from(other: config::Root) -> Self {
68 Self::new(other.into_path_buf())
69 }
70}
71
72impl From<Root> for config::Root {
73 fn from(other: Root) -> Self {
74 Self::new(other.into_path_buf())
75 }
76}
77
78pub struct Tree {
79 pub root: Root,
80 pub repos: Vec<repo::Repo>,
81}
82
83impl From<config::Tree> for Tree {
84 fn from(other: config::Tree) -> Self {
85 Self {
86 root: other.root.into(),
87 repos: other
88 .repos
89 .map(|repos| repos.into_iter().map(Into::into).collect())
90 .unwrap_or_default(),
91 }
92 }
93}
94
95#[derive(PartialEq, Eq)]
96pub struct RepoPath(PathBuf);
97
98pub fn find_unmanaged_repos(
99 root_path: &Path,
100 managed_repos: &[repo::Repo],
101) -> Result<Vec<RepoPath>, Error> {
102 let mut unmanaged_repos = Vec::new();
103
104 for path in find_repo_paths(root_path)? {
105 if !managed_repos
106 .iter()
107 .any(|r| Path::new(root_path).join(r.fullname().as_str()) == path)
108 {
109 unmanaged_repos.push(RepoPath(path));
110 }
111 }
112 Ok(unmanaged_repos)
113}
114
115pub fn sync_trees(config: config::Config, init_worktree: bool) -> Result<bool, Error> {
116 let mut failures = false;
117
118 let mut unmanaged_repos_absolute_paths = vec![];
119 let mut managed_repos_absolute_paths = vec![];
120
121 let trees: Vec<Tree> = config.get_trees()?.into_iter().map(Into::into).collect();
122
123 for tree in trees {
124 let root_path = path::expand_path(Path::new(&tree.root.0))?;
125
126 for repo in &tree.repos {
127 managed_repos_absolute_paths.push(RepoPath(root_path.join(repo.fullname().as_str())));
128 match sync_repo(&root_path, repo, init_worktree) {
129 Ok(()) => print_repo_success(repo.name.as_str(), "OK"),
130 Err(error) => {
131 print_repo_error(repo.name.as_str(), &error.to_string());
132 failures = true;
133 }
134 }
135 }
136
137 match find_unmanaged_repos(&root_path, &tree.repos) {
138 Ok(repos) => {
139 for path in repos {
140 if !unmanaged_repos_absolute_paths.contains(&path) {
141 unmanaged_repos_absolute_paths.push(path);
142 }
143 }
144 }
145 Err(error) => {
146 print_error(&format!("Error getting unmanaged repos: {error}"));
147 failures = true;
148 }
149 }
150 }
151
152 for unmanaged_repo_absolute_path in &unmanaged_repos_absolute_paths {
153 if managed_repos_absolute_paths
154 .iter()
155 .any(|managed_repo_absolute_path| {
156 managed_repo_absolute_path == unmanaged_repo_absolute_path
157 })
158 {
159 continue;
160 }
161 print_warning(format!(
162 "Found unmanaged repository: \"{}\"",
163 path::path_as_string(&unmanaged_repo_absolute_path.0)?
164 ));
165 }
166
167 Ok(!failures)
168}
169
170pub fn find_repo_paths(path: &Path) -> Result<Vec<PathBuf>, Error> {
172 let mut repos = Vec::new();
173
174 let git_dir = path.join(".git");
175 let git_worktree = path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY);
176
177 if git_dir.exists() || git_worktree.exists() {
178 repos.push(path.to_path_buf());
179 } else {
180 match fs::read_dir(path) {
181 Ok(contents) => {
182 for content in contents {
183 match content {
184 Ok(entry) => {
185 let path = entry.path();
186 if path.is_symlink() {
187 continue;
188 }
189 if path.is_dir() {
190 match find_repo_paths(&path) {
191 Ok(ref mut r) => repos.append(r),
192 Err(error) => return Err(error),
193 }
194 }
195 }
196 Err(e) => {
197 return Err(Error::DirectoryAccess {
198 message: e.to_string(),
199 });
200 }
201 }
202 }
203 }
204 Err(e) => {
205 return Err(match e.kind() {
206 std::io::ErrorKind::NotFound => Error::NotFound {
207 path: path.to_path_buf(),
208 },
209 kind => Error::Open {
210 path: path.to_path_buf(),
211 kind,
212 },
213 });
214 }
215 }
216 }
217
218 Ok(repos)
219}
220
221fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result<(), Error> {
222 let repo_path = root_path.join(repo.fullname().as_str());
223 let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
224
225 let mut newly_created = false;
226
227 if repo_path.exists() && repo_path.read_dir()?.next().is_some() {
251 if repo.worktree_setup && !actual_git_directory.exists() {
252 return Err(Error::WorktreeExpected);
253 }
254 } else if let Some(first) = repo.remotes.first() {
255 match repo::clone_repo(first, &repo_path, repo.worktree_setup) {
256 Ok(()) => {
257 print_repo_success(repo.name.as_str(), "Repository successfully cloned");
258 }
259 Err(e) => {
260 return Err(Error::CloneFailed {
261 message: e.to_string(),
262 });
263 }
264 }
265
266 newly_created = true;
267 } else {
268 print_repo_action(
269 repo.name.as_str(),
270 "Repository does not have remotes configured, initializing new",
271 );
272 match repo::RepoHandle::init(&repo_path, repo.worktree_setup) {
273 Ok(_repo_handle) => {
274 print_repo_success(repo.name.as_str(), "Repository created");
275 }
276 Err(e) => {
277 return Err(Error::InitFailed {
278 message: e.to_string(),
279 });
280 }
281 }
282 }
283
284 let repo_handle = match repo::RepoHandle::open(&repo_path, repo.worktree_setup) {
285 Ok(repo) => repo,
286 Err(error) => {
287 if !repo.worktree_setup && repo::RepoHandle::open(&repo_path, true).is_ok() {
288 return Err(Error::WorktreeNotExpected);
289 } else {
290 return Err(error.into());
291 }
292 }
293 };
294
295 if newly_created && repo.worktree_setup && init_worktree {
296 match repo_handle.default_branch() {
297 Ok(branch) => {
298 worktree::add_worktree(
299 &repo_path,
300 &WorktreeName::new(branch.name()?.into_string()),
301 None,
302 false,
303 )?;
304 }
305 Err(_error) => print_repo_error(
306 repo.name.as_str(),
307 "Could not determine default branch, skipping worktree initializtion",
308 ),
309 }
310 }
311
312 let current_remotes = repo_handle.remotes()?;
313
314 for remote in &repo.remotes {
315 let current_remote = repo_handle.find_remote(&remote.name)?;
316
317 if let Some(current_remote) = current_remote {
318 let current_url = current_remote.url()?;
319
320 if remote.url != current_url {
321 print_repo_action(
322 repo.name.as_str(),
323 &format!("Updating remote {} to \"{}\"", &remote.name, &remote.url),
324 );
325 repo_handle.remote_set_url(&remote.name, &remote.url)?;
326 }
327 } else {
328 print_repo_action(
329 repo.name.as_str(),
330 &format!(
331 "Setting up new remote \"{}\" to \"{}\"",
332 &remote.name, &remote.url
333 ),
334 );
335 repo_handle.new_remote(&remote.name, &remote.url)?;
336 }
337 }
338
339 for current_remote in ¤t_remotes {
340 if !repo.remotes.iter().any(|r| &r.name == current_remote) {
341 print_repo_action(
342 repo.name.as_str(),
343 &format!("Deleting remote \"{}\"", ¤t_remote),
344 );
345 repo_handle.remote_delete(current_remote)?;
346 }
347 }
348
349 Ok(())
350}
351
352fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf {
353 if is_worktree {
354 path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY)
355 } else {
356 path.to_path_buf()
357 }
358}