1#![forbid(unsafe_code)]
2
3use std::{
4 borrow::Cow,
5 fmt::{self, Display},
6 path::Path,
7};
8
9use thiserror::Error;
10
11pub mod auth;
12pub mod config;
13pub mod output;
14pub mod path;
15pub mod provider;
16pub mod repo;
17pub mod table;
18pub mod tree;
19pub mod worktree;
20
21#[derive(Debug, Error)]
22pub enum Error {
23 #[error(transparent)]
24 Repo(#[from] repo::Error),
25 #[error(transparent)]
26 Tree(#[from] tree::Error),
27 #[error("invalid regex: {}", .message)]
28 InvalidRegex { message: String },
29 #[error("Cannot detect root directory. Are you working in /?")]
30 CannotDetectRootDirectory,
31 #[error(transparent)]
32 Path(#[from] path::Error),
33}
34
35pub struct Warning(String);
36
37impl Display for Warning {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(f, "{}", self.0)
40 }
41}
42
43struct FindResult {
44 repos: Repos,
45 warnings: Vec<Warning>,
46}
47
48enum Repos {
49 InSearchRoot(repo::Repo),
50 List(Vec<repo::Repo>),
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct BranchName(String);
55
56impl fmt::Display for BranchName {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(f, "{}", self.0)
59 }
60}
61
62impl BranchName {
63 pub fn new(from: String) -> Self {
64 Self(from)
65 }
66
67 pub fn as_str(&self) -> &str {
68 &self.0
69 }
70
71 pub fn into_string(self) -> String {
72 self.0
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct RemoteName(Cow<'static, str>);
78
79impl fmt::Display for RemoteName {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "{}", self.0)
82 }
83}
84
85impl RemoteName {
86 pub fn new(from: String) -> Self {
87 Self(Cow::Owned(from))
88 }
89
90 pub const fn new_static(from: &'static str) -> Self {
91 Self(Cow::Borrowed(from))
92 }
93
94 pub fn as_str(&self) -> &str {
95 &self.0
96 }
97
98 pub fn into_string(self) -> String {
99 match self.0 {
100 Cow::Borrowed(s) => s.to_owned(),
101 Cow::Owned(s) => s,
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct RemoteUrl(String);
108
109impl fmt::Display for RemoteUrl {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(f, "{}", self.0)
112 }
113}
114
115impl RemoteUrl {
116 pub fn new(from: String) -> Self {
117 Self(from)
118 }
119
120 pub fn as_str(&self) -> &str {
121 &self.0
122 }
123
124 pub fn into_string(self) -> String {
125 self.0
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct SubmoduleName(String);
131
132impl fmt::Display for SubmoduleName {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(f, "{}", self.0)
135 }
136}
137
138impl SubmoduleName {
139 pub fn new(from: String) -> Self {
140 Self(from)
141 }
142
143 pub fn as_str(&self) -> &str {
144 &self.0
145 }
146
147 pub fn into_string(self) -> String {
148 self.0
149 }
150}
151
152fn find_repos(root: &Path, exclusion_pattern: Option<®ex::Regex>) -> Result<FindResult, Error> {
154 let mut repos: Vec<repo::Repo> = Vec::new();
155 let mut repo_in_root = false;
156 let mut warnings = Vec::new();
157
158 for path in tree::find_repo_paths(root)? {
159 if exclusion_pattern
160 .as_ref()
161 .map(|regex| -> Result<bool, Error> {
162 Ok(regex.is_match(&path::path_as_string(&path)?))
163 })
164 .transpose()?
165 .unwrap_or(false)
166 {
167 warnings.push(Warning(format!(
168 "[skipped] {}",
169 &path::path_as_string(&path)?
170 )));
171 continue;
172 }
173
174 let is_worktree = repo::RepoHandle::detect_worktree(&path);
175 if path == root {
176 repo_in_root = true;
177 }
178
179 match repo::RepoHandle::open(&path, is_worktree) {
180 Err(error) => {
181 warnings.push(Warning(format!(
182 "Error opening repo {}{}: {}",
183 path.display(),
184 if is_worktree { " as worktree" } else { "" },
185 error
186 )));
187 }
188 Ok(repo) => {
189 let remotes = match repo.remotes() {
190 Ok(remote) => remote,
191 Err(error) => {
192 warnings.push(Warning(format!(
193 "{}: Error getting remotes: {}",
194 &path::path_as_string(&path)?,
195 error
196 )));
197 continue;
198 }
199 };
200
201 let mut results: Vec<repo::Remote> = Vec::new();
202 for remote_name in remotes {
203 match repo.find_remote(&remote_name)? {
204 Some(remote) => {
205 let name = remote.name()?;
206 let url = remote.url()?;
207 let remote_type = match repo::detect_remote_type(&url) {
208 Ok(t) => t,
209 Err(e) => {
210 warnings.push(Warning(format!(
211 "{}: Could not handle URL {}. Reason: {}",
212 &path::path_as_string(&path)?,
213 &url,
214 e
215 )));
216 continue;
217 }
218 };
219
220 results.push(repo::Remote {
221 name,
222 url,
223 remote_type,
224 });
225 }
226 None => {
227 warnings.push(Warning(format!(
228 "{}: Remote {} not found",
229 &path::path_as_string(&path)?,
230 remote_name
231 )));
232 }
233 }
234 }
235 let remotes = results;
236
237 let (namespace, name) = if path == root {
238 (
239 None,
240 if let Some(parent) = root.parent() {
241 path::path_as_string(
242 path.strip_prefix(parent)
243 .expect("checked for prefix explicitly above"),
244 )?
245 } else {
246 warnings.push(Warning(String::from("Getting name of the search root failed. Do you have a git repository in \"/\"?")));
247 continue;
248 },
249 )
250 } else {
251 let name = path
252 .strip_prefix(root)
253 .expect("checked for prefix explicitly above");
254 let namespace = name.parent().expect("path always has a parent");
255 (
256 if namespace != Path::new("") {
257 Some(path::path_as_string(namespace)?.clone())
258 } else {
259 None
260 },
261 path::path_as_string(name)?,
262 )
263 };
264
265 repos.push(repo::Repo {
266 name: repo::ProjectName::new(name),
267 namespace: namespace.map(repo::ProjectNamespace::new),
268 remotes,
269 worktree_setup: is_worktree,
270 });
271 }
272 }
273 }
274 Ok(FindResult {
275 repos: if repo_in_root {
276 #[expect(clippy::panic, reason = "potential bug")]
277 Repos::InSearchRoot(if repos.len() != 1 {
278 panic!("found multiple repos in root?")
279 } else {
280 repos
281 .pop()
282 .expect("checked len() above and list cannot be empty")
283 })
284 } else {
285 Repos::List(repos)
286 },
287 warnings,
288 })
289}
290
291pub fn find_in_tree(
292 path: &Path,
293 exclusion_pattern: Option<®ex::Regex>,
294) -> Result<(tree::Tree, Vec<Warning>), Error> {
295 let mut warnings = Vec::new();
296
297 let mut result = find_repos(path, exclusion_pattern)?;
298
299 warnings.append(&mut result.warnings);
300
301 let (root, repos) = match result.repos {
302 Repos::InSearchRoot(repo) => (
303 path.parent()
304 .ok_or(Error::CannotDetectRootDirectory)?
305 .to_path_buf(),
306 vec![repo],
307 ),
308 Repos::List(repos) => (path.to_path_buf(), repos),
309 };
310
311 Ok((
312 tree::Tree {
313 root: tree::Root::new(root),
314 repos,
315 },
316 warnings,
317 ))
318}