1use std::{fmt, fs, sync::mpsc};
5
6use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
7use thiserror::Error;
8
9use super::{
10 RemoteName, RemoteUrl, SyncTreesMessage, config, path,
11 repo::{self, RepoName, TrackingSelection, WorktreeName, WorktreeRepoHandle, WorktreeSetup},
12 send_msg,
13};
14
15#[derive(Debug, Error)]
16pub enum Error {
17 #[error(transparent)]
18 Config(#[from] config::Error),
19 #[error(transparent)]
20 Repo(#[from] repo::Error),
21 #[error(transparent)]
22 Worktree(#[from] repo::WorktreeError),
23 #[error("Failed to open \"{path}\": Not found")]
24 NotFound { path: PathBuf },
25 #[error("Failed to open \"{path}\": {kind}")]
26 Open {
27 path: PathBuf,
28 kind: std::io::ErrorKind,
29 },
30 #[error(transparent)]
31 Io(#[from] std::io::Error),
32 #[error("Error accessing directory: {message}")]
33 DirectoryAccess { message: String },
34 #[error("Repo already exists, but is not using a worktree setup")]
35 WorktreeExpected,
36 #[error("Repo already exists, but is using a worktree setup")]
37 WorktreeNotExpected,
38 #[error("Repository failed during init: {message}")]
39 InitFailed { message: String },
40 #[error("Repository failed during clone: {message}")]
41 CloneFailed { message: String },
42 #[error("Could not get trees from config: {message}")]
43 TreesFromConfig { message: String },
44 #[error(transparent)]
45 Path(#[from] path::Error),
46 #[error(transparent)]
47 WorktreeValidation(#[from] repo::WorktreeValidationError),
48}
49
50#[derive(Debug)]
51pub struct Root(PathBuf);
52
53impl Root {
54 pub fn new(s: PathBuf) -> Self {
55 Self(s)
56 }
57
58 pub fn as_path(&self) -> &Path {
59 &self.0
60 }
61
62 pub fn into_path_buf(self) -> PathBuf {
63 self.0
64 }
65}
66
67impl From<config::Root> for Root {
68 fn from(other: config::Root) -> Self {
69 Self::new(other.into_path_buf())
70 }
71}
72
73impl From<Root> for config::Root {
74 fn from(other: Root) -> Self {
75 Self::new(other.into_path_buf())
76 }
77}
78
79pub struct Tree {
80 pub root: Root,
81 pub repos: Vec<repo::Repo>,
82}
83
84impl From<config::Tree> for Tree {
85 fn from(other: config::Tree) -> Self {
86 Self {
87 root: other.root.into(),
88 repos: other
89 .repos
90 .map(|repos| repos.into_iter().map(Into::into).collect())
91 .unwrap_or_default(),
92 }
93 }
94}
95
96#[derive(PartialEq, Eq, Debug)]
97pub struct RepoPath(PathBuf);
98
99impl RepoPath {
100 pub fn as_path(&self) -> &Path {
101 self.0.as_path()
102 }
103}
104
105impl fmt::Display for RepoPath {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 write!(f, "{}", self.0)
108 }
109}
110
111pub fn find_unmanaged_repos(
112 root_path: &Path,
113 managed_repos: &[repo::Repo],
114) -> Result<Vec<RepoPath>, Error> {
115 let mut unmanaged_repos = Vec::new();
116
117 for path in find_repo_paths(root_path)? {
118 if !managed_repos
119 .iter()
120 .any(|r| Path::new(root_path).join(r.fullname().as_str()) == path)
121 {
122 unmanaged_repos.push(RepoPath(path));
123 }
124 }
125 Ok(unmanaged_repos)
126}
127
128#[derive(PartialEq, Eq, Copy, Clone)]
129pub enum OperationResult {
130 Success,
131 Failure,
132}
133
134impl OperationResult {
135 pub fn is_success(self) -> bool {
136 self == Self::Success
137 }
138
139 pub fn is_failure(self) -> bool {
140 !self.is_success()
141 }
142}
143
144pub enum SyncTreeMessage {
145 Cloning((PathBuf, RemoteUrl)),
146 Cloned(RepoName),
147 Init(RepoName),
148 Created(RepoName),
149 SyncDone(RepoName),
150 SkippingWorktreeInit(RepoName),
151 UpdatingRemote((RepoName, RemoteName, RemoteUrl)),
152 CreateRemote((RepoName, RemoteName, RemoteUrl)),
153 DeleteRemote((RepoName, RemoteName)),
154}
155
156pub fn sync_trees(
157 trees: Vec<Tree>,
158 init_worktree: bool,
159 result_channel: &mpsc::SyncSender<SyncTreesMessage>,
160) -> Result<(OperationResult, Vec<RepoPath>), Error> {
161 let mut failures = false;
162
163 let mut unmanaged_repos = vec![];
164 let mut managed_repos = vec![];
165
166 for tree in trees {
167 let root_path = path::expand_path(Path::new(&tree.root.0))?;
168
169 for repo in &tree.repos {
170 managed_repos.push(RepoPath(root_path.join(repo.fullname().as_str())));
171 match sync_repo(&root_path, repo, init_worktree, result_channel) {
172 Ok(()) => {
173 send_msg(
174 result_channel,
175 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::SyncDone(
176 repo.name.clone(),
177 ))),
178 );
179 }
180 Err(error) => {
181 send_msg(
182 result_channel,
183 SyncTreesMessage::SyncTreeMessage(Err((repo.name.clone(), error.into()))),
184 );
185 failures = true;
186 }
187 }
188 }
189
190 unmanaged_repos.extend(find_unmanaged_repos(&root_path, &tree.repos)?);
191 }
192
193 unmanaged_repos.retain(|unmanaged_path| {
197 !managed_repos
198 .iter()
199 .any(|managed_path| unmanaged_path == managed_path)
200 });
201
202 Ok((
203 if failures {
204 OperationResult::Failure
205 } else {
206 OperationResult::Success
207 },
208 unmanaged_repos,
209 ))
210}
211
212fn is_repo_root(path: &Path) -> bool {
215 path.join(".git").exists() || path.join(repo::GIT_MAIN_WORKTREE_DIRECTORY).exists()
216}
217
218pub fn find_repo_paths(path: &Path) -> Result<Vec<PathBuf>, Error> {
220 let mut repos = Vec::new();
221
222 if is_repo_root(path) {
223 repos.push(path.to_path_buf());
224 } else {
225 match fs::read_dir(path) {
226 Ok(contents) => {
227 for content in contents {
228 match content {
229 Ok(entry) => {
230 let path = path::from_std_path_buf(entry.path())?;
231 if path.is_symlink() {
232 continue;
233 }
234 if path.is_dir() {
235 {
236 let r = &mut find_repo_paths(&path)?;
237 repos.append(r);
238 }
239 }
240 }
241 Err(e) => {
242 return Err(Error::DirectoryAccess {
243 message: e.to_string(),
244 });
245 }
246 }
247 }
248 }
249 Err(e) => {
250 return Err(match e.kind() {
251 std::io::ErrorKind::NotFound => Error::NotFound {
252 path: path.to_path_buf(),
253 },
254 kind => Error::Open {
255 path: path.to_path_buf(),
256 kind,
257 },
258 });
259 }
260 }
261 }
262
263 Ok(repos)
264}
265
266fn find_unmanaged_files_in(path: &Path) -> Result<(bool, Vec<PathBuf>), Error> {
278 if is_repo_root(path) {
279 return Ok((true, Vec::new()));
280 }
281
282 let contents = match fs::read_dir(path) {
283 Ok(contents) => contents,
284 Err(e) => {
285 return Err(match e.kind() {
286 std::io::ErrorKind::NotFound => Error::NotFound {
287 path: path.to_path_buf(),
288 },
289 kind => Error::Open {
290 path: path.to_path_buf(),
291 kind,
292 },
293 });
294 }
295 };
296
297 let mut contains_repo = false;
298 let mut unmanaged = Vec::new();
299
300 for content in contents {
301 let entry = content.map_err(|e| Error::DirectoryAccess {
302 message: e.to_string(),
303 })?;
304 let entry_path = path::from_std_path_buf(entry.path())?;
305
306 if entry_path.is_dir() && !entry_path.is_symlink() {
307 let (sub_contains_repo, sub_unmanaged) = find_unmanaged_files_in(&entry_path)?;
308 if sub_contains_repo {
309 contains_repo = true;
310 unmanaged.extend(sub_unmanaged);
311 } else {
312 unmanaged.push(entry_path);
313 }
314 } else {
315 unmanaged.push(entry_path);
316 }
317 }
318
319 Ok((contains_repo, unmanaged))
320}
321
322pub fn find_unmanaged_files(root_path: &Path) -> Result<Vec<PathBuf>, Error> {
328 let (_contains_repo, mut unmanaged) = find_unmanaged_files_in(root_path)?;
329
330 unmanaged.sort();
331
332 Ok(unmanaged)
333}
334
335fn sync_repo(
336 root_path: &Path,
337 repo: &repo::Repo,
338 init_worktree: bool,
339 result_channel: &mpsc::SyncSender<SyncTreesMessage>,
340) -> Result<(), Error> {
341 let repo_path = root_path.join(repo.fullname().as_str());
342 let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
343
344 let mut newly_created = false;
345
346 if repo_path.exists() && repo_path.read_dir()?.next().is_some() {
370 if repo.worktree_setup.is_worktree() && !actual_git_directory.exists() {
371 return Err(Error::WorktreeExpected);
372 }
373 } else if let Some(first) = repo.remotes.first() {
374 send_msg(
375 result_channel,
376 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::Cloning((
377 repo_path.clone(),
378 first.url.clone(),
379 )))),
380 );
381
382 match repo::clone_repo(first, &repo_path, repo.worktree_setup) {
383 Ok(()) => send_msg(
384 result_channel,
385 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::Cloned(repo.name.clone()))),
386 ),
387 Err(e) => {
388 return Err(Error::CloneFailed {
389 message: e.to_string(),
390 });
391 }
392 }
393
394 newly_created = true;
395 } else {
396 send_msg(
397 result_channel,
398 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::Init(repo.name.clone()))),
399 );
400 match repo::RepoHandle::init(&repo_path, repo.worktree_setup) {
401 Ok(_repo_handle) => {
402 send_msg(
403 result_channel,
404 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::Created(
405 repo.name.clone(),
406 ))),
407 );
408 }
409 Err(e) => {
410 return Err(Error::InitFailed {
411 message: e.to_string(),
412 });
413 }
414 }
415 }
416
417 let repo_handle =
418 match repo::RepoHandle::open_with_worktree_setup(&repo_path, repo.worktree_setup) {
419 Ok(repo) => repo,
420 Err(error) => {
421 if !repo.worktree_setup.is_worktree()
422 && repo::WorktreeRepoHandle::open(&repo_path).is_ok()
423 {
424 return Err(Error::WorktreeNotExpected);
425 } else {
426 return Err(error.into());
427 }
428 }
429 };
430
431 let repo_handle = if newly_created && repo.worktree_setup.is_worktree() && init_worktree {
432 let repo_handle = WorktreeRepoHandle::from_handle_unchecked(repo_handle);
433
434 match repo_handle.default_branch() {
435 Ok(branch) => {
436 repo_handle.add_worktree(
437 &WorktreeName::new(branch.name()?.into_string())?,
438 TrackingSelection::Automatic,
439 )?;
440 }
441 Err(_error) => send_msg(
442 result_channel,
443 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::SkippingWorktreeInit(
444 repo.name.clone(),
445 ))),
446 ),
447 }
448
449 repo_handle.into_handle()
450 } else {
451 repo_handle
452 };
453
454 let current_remotes = repo_handle.remotes()?;
455
456 for remote in &repo.remotes {
457 let current_remote = repo_handle.find_remote(&remote.name)?;
458
459 if let Some(current_remote) = current_remote {
460 let current_url = current_remote.url()?;
461
462 if remote.url != current_url {
463 send_msg(
464 result_channel,
465 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::UpdatingRemote((
466 repo.name.clone(),
467 remote.name.clone(),
468 remote.url.clone(),
469 )))),
470 );
471 repo_handle.remote_set_url(&remote.name, &remote.url)?;
472 }
473 } else {
474 send_msg(
475 result_channel,
476 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::CreateRemote((
477 repo.name.clone(),
478 remote.name.clone(),
479 remote.url.clone(),
480 )))),
481 );
482 repo_handle.new_remote(&remote.name, &remote.url)?;
483 }
484 }
485
486 for current_remote in ¤t_remotes {
487 if !repo.remotes.iter().any(|r| &r.name == current_remote) {
488 send_msg(
489 result_channel,
490 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::DeleteRemote((
491 repo.name.clone(),
492 current_remote.clone(),
493 )))),
494 );
495 repo_handle.remote_delete(current_remote)?;
496 }
497 }
498
499 Ok(())
500}
501
502fn get_actual_git_directory(path: &Path, worktree_setup: WorktreeSetup) -> PathBuf {
503 if worktree_setup.is_worktree() {
504 path.join(repo::GIT_MAIN_WORKTREE_DIRECTORY)
505 } else {
506 path.to_path_buf()
507 }
508}