grm/
tree.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use super::config;
5use super::output::*;
6use super::path;
7use super::repo;
8use super::worktree;
9
10pub struct Tree {
11    pub root: String,
12    pub repos: Vec<repo::Repo>,
13}
14
15pub fn find_unmanaged_repos(
16    root_path: &Path,
17    managed_repos: &[repo::Repo],
18) -> Result<Vec<PathBuf>, String> {
19    let mut unmanaged_repos = Vec::new();
20
21    for repo_path in find_repo_paths(root_path)? {
22        if !managed_repos
23            .iter()
24            .any(|r| Path::new(root_path).join(r.fullname()) == repo_path)
25        {
26            unmanaged_repos.push(repo_path);
27        }
28    }
29    Ok(unmanaged_repos)
30}
31
32pub fn sync_trees(config: config::Config, init_worktree: bool) -> Result<bool, String> {
33    let mut failures = false;
34
35    let mut unmanaged_repos_absolute_paths = vec![];
36    let mut managed_repos_absolute_paths = vec![];
37
38    let trees = config.trees()?;
39
40    for tree in trees {
41        let repos: Vec<repo::Repo> = tree
42            .repos
43            .unwrap_or_default()
44            .into_iter()
45            .map(|repo| repo.into_repo())
46            .collect();
47
48        let root_path = path::expand_path(Path::new(&tree.root));
49
50        for repo in &repos {
51            managed_repos_absolute_paths.push(root_path.join(repo.fullname()));
52            match sync_repo(&root_path, repo, init_worktree) {
53                Ok(()) => print_repo_success(&repo.name, "OK"),
54                Err(error) => {
55                    print_repo_error(&repo.name, &error);
56                    failures = true;
57                }
58            }
59        }
60
61        match find_unmanaged_repos(&root_path, &repos) {
62            Ok(repos) => {
63                for path in repos {
64                    if !unmanaged_repos_absolute_paths.contains(&path) {
65                        unmanaged_repos_absolute_paths.push(path);
66                    }
67                }
68            }
69            Err(error) => {
70                print_error(&format!("Error getting unmanaged repos: {error}"));
71                failures = true;
72            }
73        }
74    }
75
76    for unmanaged_repo_absolute_path in &unmanaged_repos_absolute_paths {
77        if managed_repos_absolute_paths
78            .iter()
79            .any(|managed_repo_absolute_path| {
80                managed_repo_absolute_path == unmanaged_repo_absolute_path
81            })
82        {
83            continue;
84        }
85        print_warning(&format!(
86            "Found unmanaged repository: \"{}\"",
87            path::path_as_string(unmanaged_repo_absolute_path)
88        ));
89    }
90
91    Ok(!failures)
92}
93
94/// Finds repositories recursively, returning their path
95pub fn find_repo_paths(path: &Path) -> Result<Vec<PathBuf>, String> {
96    let mut repos = Vec::new();
97
98    let git_dir = path.join(".git");
99    let git_worktree = path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY);
100
101    if git_dir.exists() || git_worktree.exists() {
102        repos.push(path.to_path_buf());
103    } else {
104        match fs::read_dir(path) {
105            Ok(contents) => {
106                for content in contents {
107                    match content {
108                        Ok(entry) => {
109                            let path = entry.path();
110                            if path.is_symlink() {
111                                continue;
112                            }
113                            if path.is_dir() {
114                                match find_repo_paths(&path) {
115                                    Ok(ref mut r) => repos.append(r),
116                                    Err(error) => return Err(error),
117                                }
118                            }
119                        }
120                        Err(error) => {
121                            return Err(format!("Error accessing directory: {error}"));
122                        }
123                    };
124                }
125            }
126            Err(e) => {
127                return Err(format!(
128                    "Failed to open \"{}\": {}",
129                    &path.display(),
130                    match e.kind() {
131                        std::io::ErrorKind::NotFound => String::from("not found"),
132                        _ => format!("{:?}", e.kind()),
133                    }
134                ));
135            }
136        };
137    }
138
139    Ok(repos)
140}
141
142fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result<(), String> {
143    let repo_path = root_path.join(repo.fullname());
144    let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
145
146    let mut newly_created = false;
147
148    // Syncing a repository can have a few different flows, depending on the repository
149    // that is to be cloned and the local directory:
150    //
151    // * If the local directory already exists, we have to make sure that it matches the
152    //   worktree configuration, as there is no way to convert. If the sync is supposed
153    //   to be worktree-aware, but the local directory is not, we abort. Note that we could
154    //   also automatically convert here. In any case, the other direction (converting a
155    //   worktree repository to non-worktree) cannot work, as we'd have to throw away the
156    //   worktrees.
157    //
158    // * If the local directory does not yet exist, we have to actually do something ;). If
159    //   no remote is specified, we just initialize a new repository (git init) and are done.
160    //
161    //   If there are (potentially multiple) remotes configured, we have to clone. We assume
162    //   that the first remote is the canonical one that we do the first clone from. After
163    //   cloning, we just add the other remotes as usual (as if they were added to the config
164    //   afterwards)
165    //
166    // Branch handling:
167    //
168    // Handling the branches on checkout is a bit magic. For minimum surprises, we just set
169    // up local tracking branches for all remote branches.
170    if repo_path.exists()
171        && repo_path
172            .read_dir()
173            .map_err(|error| error.to_string())?
174            .next()
175            .is_some()
176    {
177        if repo.worktree_setup && !actual_git_directory.exists() {
178            return Err(String::from(
179                "Repo already exists, but is not using a worktree setup",
180            ));
181        };
182    } else if repo.remotes.is_none() || repo.remotes.as_ref().unwrap().is_empty() {
183        print_repo_action(
184            &repo.name,
185            "Repository does not have remotes configured, initializing new",
186        );
187        match repo::RepoHandle::init(&repo_path, repo.worktree_setup) {
188            Ok(r) => {
189                print_repo_success(&repo.name, "Repository created");
190                Some(r)
191            }
192            Err(error) => {
193                return Err(format!("Repository failed during init: {error}"));
194            }
195        };
196    } else {
197        let first = repo.remotes.as_ref().unwrap().first().unwrap();
198
199        match repo::clone_repo(first, &repo_path, repo.worktree_setup) {
200            Ok(()) => {
201                print_repo_success(&repo.name, "Repository successfully cloned");
202            }
203            Err(error) => {
204                return Err(format!("Repository failed during clone: {error}"));
205            }
206        };
207
208        newly_created = true;
209    }
210
211    let repo_handle = match repo::RepoHandle::open(&repo_path, repo.worktree_setup) {
212        Ok(repo) => repo,
213        Err(error) => {
214            return if !repo.worktree_setup && repo::RepoHandle::open(&repo_path, true).is_ok() {
215                Err(String::from(
216                    "Repo already exists, but is using a worktree setup",
217                ))
218            } else {
219                Err(format!("Opening repository failed: {error}"))
220            }
221        }
222    };
223
224    if newly_created && repo.worktree_setup && init_worktree {
225        match repo_handle.default_branch() {
226            Ok(branch) => {
227                worktree::add_worktree(&repo_path, &branch.name()?, None, false)?;
228            }
229            Err(_error) => print_repo_error(
230                &repo.name,
231                "Could not determine default branch, skipping worktree initializtion",
232            ),
233        }
234    }
235    if let Some(remotes) = &repo.remotes {
236        let current_remotes: Vec<String> = repo_handle
237            .remotes()
238            .map_err(|error| format!("Repository failed during getting the remotes: {error}"))?;
239
240        for remote in remotes {
241            let current_remote = repo_handle.find_remote(&remote.name)?;
242
243            match current_remote {
244                Some(current_remote) => {
245                    let current_url = current_remote.url();
246
247                    if remote.url != current_url {
248                        print_repo_action(
249                            &repo.name,
250                            &format!("Updating remote {} to \"{}\"", &remote.name, &remote.url),
251                        );
252                        if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) {
253                            return Err(format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e));
254                        };
255                    }
256                }
257                None => {
258                    print_repo_action(
259                        &repo.name,
260                        &format!(
261                            "Setting up new remote \"{}\" to \"{}\"",
262                            &remote.name, &remote.url
263                        ),
264                    );
265                    if let Err(error) = repo_handle.new_remote(&remote.name, &remote.url) {
266                        return Err(format!(
267                            "Repository failed during setting the remotes: {error}",
268                        ));
269                    }
270                }
271            }
272        }
273
274        for current_remote in &current_remotes {
275            if !remotes.iter().any(|r| &r.name == current_remote) {
276                print_repo_action(
277                    &repo.name,
278                    &format!("Deleting remote \"{}\"", &current_remote,),
279                );
280                if let Err(e) = repo_handle.remote_delete(current_remote) {
281                    return Err(format!(
282                        "Repository failed during deleting remote \"{}\": {}",
283                        &current_remote, e
284                    ));
285                }
286            }
287        }
288    }
289    Ok(())
290}
291
292fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf {
293    if is_worktree {
294        path.join(worktree::GIT_MAIN_WORKTREE_DIRECTORY)
295    } else {
296        path.to_path_buf()
297    }
298}