1#![forbid(unsafe_code)]
2
3use std::path::Path;
4
5pub mod auth;
6pub mod config;
7pub mod output;
8pub mod path;
9pub mod provider;
10pub mod repo;
11pub mod table;
12pub mod tree;
13pub mod worktree;
14
15#[allow(clippy::type_complexity)]
20fn find_repos(
21 root: &Path,
22 exclusion_pattern: Option<&str>,
23) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)>, String> {
24 let mut repos: Vec<repo::Repo> = Vec::new();
25 let mut repo_in_root = false;
26 let mut warnings = Vec::new();
27
28 let exlusion_regex: regex::Regex = regex::Regex::new(exclusion_pattern.unwrap_or(r"^$"))
29 .map_err(|e| format!("invalid regex: {e}"))?;
30 for path in tree::find_repo_paths(root)? {
31 if exclusion_pattern.is_some() && exlusion_regex.is_match(&path::path_as_string(&path)) {
32 warnings.push(format!("[skipped] {}", &path::path_as_string(&path)));
33 continue;
34 }
35
36 let is_worktree = repo::RepoHandle::detect_worktree(&path);
37 if path == root {
38 repo_in_root = true;
39 }
40
41 match repo::RepoHandle::open(&path, is_worktree) {
42 Err(error) => {
43 warnings.push(format!(
44 "Error opening repo {}{}: {}",
45 path.display(),
46 if is_worktree { " as worktree" } else { "" },
47 error
48 ));
49 continue;
50 }
51 Ok(repo) => {
52 let remotes = match repo.remotes() {
53 Ok(remote) => remote,
54 Err(error) => {
55 warnings.push(format!(
56 "{}: Error getting remotes: {}",
57 &path::path_as_string(&path),
58 error
59 ));
60 continue;
61 }
62 };
63
64 let mut results: Vec<repo::Remote> = Vec::new();
65 for remote_name in remotes {
66 match repo.find_remote(&remote_name)? {
67 Some(remote) => {
68 let name = remote.name();
69 let url = remote.url();
70 let remote_type = match repo::detect_remote_type(&url) {
71 Ok(t) => t,
72 Err(e) => {
73 warnings.push(format!(
74 "{}: Could not handle URL {}. Reason: {}",
75 &path::path_as_string(&path),
76 &url,
77 e
78 ));
79 continue;
80 }
81 };
82
83 results.push(repo::Remote {
84 name,
85 url,
86 remote_type,
87 });
88 }
89 None => {
90 warnings.push(format!(
91 "{}: Remote {} not found",
92 &path::path_as_string(&path),
93 remote_name
94 ));
95 continue;
96 }
97 };
98 }
99 let remotes = results;
100
101 let (namespace, name) = if path == root {
102 (
103 None,
104 match &root.parent() {
105 Some(parent) => {
106 path::path_as_string(path.strip_prefix(parent).unwrap())
107 }
108 None => {
109 warnings.push(String::from("Getting name of the search root failed. Do you have a git repository in \"/\"?"));
110 continue;
111 }
112 },
113 )
114 } else {
115 let name = path.strip_prefix(root).unwrap();
116 let namespace = name.parent().unwrap();
117 (
118 if namespace == Path::new("") {
119 None
120 } else {
121 Some(path::path_as_string(namespace).to_string())
122 },
123 path::path_as_string(name),
124 )
125 };
126
127 repos.push(repo::Repo {
128 name,
129 namespace,
130 remotes: Some(remotes),
131 worktree_setup: is_worktree,
132 });
133 }
134 }
135 }
136 Ok(Some((repos, warnings, repo_in_root)))
137}
138
139pub fn find_in_tree(
140 path: &Path,
141 exclusion_pattern: Option<&str>,
142) -> Result<(tree::Tree, Vec<String>), String> {
143 let mut warnings = Vec::new();
144
145 let (repos, repo_in_root): (Vec<repo::Repo>, bool) = match find_repos(path, exclusion_pattern)?
146 {
147 Some((vec, mut repo_warnings, repo_in_root)) => {
148 warnings.append(&mut repo_warnings);
149 (vec, repo_in_root)
150 }
151 None => (Vec::new(), false),
152 };
153
154 let mut root = path.to_path_buf();
155 if repo_in_root {
156 root = match root.parent() {
157 Some(root) => root.to_path_buf(),
158 None => {
159 return Err(String::from(
160 "Cannot detect root directory. Are you working in /?",
161 ));
162 }
163 }
164 }
165
166 Ok((
167 tree::Tree {
168 root: root.into_os_string().into_string().unwrap(),
169 repos,
170 },
171 warnings,
172 ))
173}