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
212pub fn find_repo_paths(path: &Path) -> Result<Vec<PathBuf>, Error> {
214 let mut repos = Vec::new();
215
216 let git_dir = path.join(".git");
217 let git_worktree = path.join(repo::GIT_MAIN_WORKTREE_DIRECTORY);
218
219 if git_dir.exists() || git_worktree.exists() {
220 repos.push(path.to_path_buf());
221 } else {
222 match fs::read_dir(path) {
223 Ok(contents) => {
224 for content in contents {
225 match content {
226 Ok(entry) => {
227 let path = path::from_std_path_buf(entry.path())?;
228 if path.is_symlink() {
229 continue;
230 }
231 if path.is_dir() {
232 match find_repo_paths(&path) {
233 Ok(ref mut r) => repos.append(r),
234 Err(error) => return Err(error),
235 }
236 }
237 }
238 Err(e) => {
239 return Err(Error::DirectoryAccess {
240 message: e.to_string(),
241 });
242 }
243 }
244 }
245 }
246 Err(e) => {
247 return Err(match e.kind() {
248 std::io::ErrorKind::NotFound => Error::NotFound {
249 path: path.to_path_buf(),
250 },
251 kind => Error::Open {
252 path: path.to_path_buf(),
253 kind,
254 },
255 });
256 }
257 }
258 }
259
260 Ok(repos)
261}
262
263fn sync_repo(
264 root_path: &Path,
265 repo: &repo::Repo,
266 init_worktree: bool,
267 result_channel: &mpsc::SyncSender<SyncTreesMessage>,
268) -> Result<(), Error> {
269 let repo_path = root_path.join(repo.fullname().as_str());
270 let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
271
272 let mut newly_created = false;
273
274 if repo_path.exists() && repo_path.read_dir()?.next().is_some() {
298 if repo.worktree_setup.is_worktree() && !actual_git_directory.exists() {
299 return Err(Error::WorktreeExpected);
300 }
301 } else if let Some(first) = repo.remotes.first() {
302 send_msg(
303 result_channel,
304 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::Cloning((
305 repo_path.clone(),
306 first.url.clone(),
307 )))),
308 );
309
310 match repo::clone_repo(first, &repo_path, repo.worktree_setup) {
311 Ok(()) => send_msg(
312 result_channel,
313 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::Cloned(repo.name.clone()))),
314 ),
315 Err(e) => {
316 return Err(Error::CloneFailed {
317 message: e.to_string(),
318 });
319 }
320 }
321
322 newly_created = true;
323 } else {
324 send_msg(
325 result_channel,
326 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::Init(repo.name.clone()))),
327 );
328 match repo::RepoHandle::init(&repo_path, repo.worktree_setup) {
329 Ok(_repo_handle) => {
330 send_msg(
331 result_channel,
332 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::Created(
333 repo.name.clone(),
334 ))),
335 );
336 }
337 Err(e) => {
338 return Err(Error::InitFailed {
339 message: e.to_string(),
340 });
341 }
342 }
343 }
344
345 let repo_handle =
346 match repo::RepoHandle::open_with_worktree_setup(&repo_path, repo.worktree_setup) {
347 Ok(repo) => repo,
348 Err(error) => {
349 if !repo.worktree_setup.is_worktree()
350 && repo::WorktreeRepoHandle::open(&repo_path).is_ok()
351 {
352 return Err(Error::WorktreeNotExpected);
353 } else {
354 return Err(error.into());
355 }
356 }
357 };
358
359 let repo_handle = if newly_created && repo.worktree_setup.is_worktree() && init_worktree {
360 let repo_handle = WorktreeRepoHandle::from_handle_unchecked(repo_handle);
361
362 match repo_handle.default_branch() {
363 Ok(branch) => {
364 repo_handle.add_worktree(
365 &WorktreeName::new(branch.name()?.into_string())?,
366 TrackingSelection::Automatic,
367 )?;
368 }
369 Err(_error) => send_msg(
370 result_channel,
371 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::SkippingWorktreeInit(
372 repo.name.clone(),
373 ))),
374 ),
375 }
376
377 repo_handle.into_handle()
378 } else {
379 repo_handle
380 };
381
382 let current_remotes = repo_handle.remotes()?;
383
384 for remote in &repo.remotes {
385 let current_remote = repo_handle.find_remote(&remote.name)?;
386
387 if let Some(current_remote) = current_remote {
388 let current_url = current_remote.url()?;
389
390 if remote.url != current_url {
391 send_msg(
392 result_channel,
393 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::UpdatingRemote((
394 repo.name.clone(),
395 remote.name.clone(),
396 remote.url.clone(),
397 )))),
398 );
399 repo_handle.remote_set_url(&remote.name, &remote.url)?;
400 }
401 } else {
402 send_msg(
403 result_channel,
404 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::CreateRemote((
405 repo.name.clone(),
406 remote.name.clone(),
407 remote.url.clone(),
408 )))),
409 );
410 repo_handle.new_remote(&remote.name, &remote.url)?;
411 }
412 }
413
414 for current_remote in ¤t_remotes {
415 if !repo.remotes.iter().any(|r| &r.name == current_remote) {
416 send_msg(
417 result_channel,
418 SyncTreesMessage::SyncTreeMessage(Ok(SyncTreeMessage::DeleteRemote((
419 repo.name.clone(),
420 current_remote.clone(),
421 )))),
422 );
423 repo_handle.remote_delete(current_remote)?;
424 }
425 }
426
427 Ok(())
428}
429
430fn get_actual_git_directory(path: &Path, worktree_setup: WorktreeSetup) -> PathBuf {
431 if worktree_setup.is_worktree() {
432 path.join(repo::GIT_MAIN_WORKTREE_DIRECTORY)
433 } else {
434 path.to_path_buf()
435 }
436}