git_repos/
tree.rs

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
82/// Finds repositories recursively, returning their path
83pub 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    // Syncing a repository can have a few different flows, depending on the repository
133    // that is to be cloned and the local directory:
134    //
135    // * If the local directory already exists, we have to make sure that it matches the
136    //   worktree configuration, as there is no way to convert. If the sync is supposed
137    //   to be worktree-aware, but the local directory is not, we abort. Note that we could
138    //   also automatically convert here. In any case, the other direction (converting a
139    //   worktree repository to non-worktree) cannot work, as we'd have to throw away the
140    //   worktrees.
141    //
142    // * If the local directory does not yet exist, we have to actually do something ;). If
143    //   no remote is specified, we just initialize a new repository (git init) and are done.
144    //
145    //   If there are (potentially multiple) remotes configured, we have to clone. We assume
146    //   that the first remote is the canonical one that we do the first clone from. After
147    //   cloning, we just add the other remotes as usual (as if they were added to the config
148    //   afterwards)
149    //
150    // Branch handling:
151    //
152    // Handling the branches on checkout is a bit magic. For minimum surprises, we just set
153    // up local tracking branches for all remote branches.
154    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 &current_remotes {
254            if !remotes.iter().any(|r| &r.name == current_remote) {
255                print_repo_action(&repo.name, &format!("Deleting remote \"{}\"", &current_remote,));
256                if let Err(e) = repo_handle.remote_delete(current_remote) {
257                    return Err(format!(
258                        "Repository failed during deleting remote \"{}\": {}",
259                        &current_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}