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