1use super::config;
2use super::path;
3use super::repo;
4
5use comfy_table::{Cell, Table};
6
7use std::fmt::Write;
8use std::path::Path;
9
10fn add_table_header(table: &mut Table) {
11 table
12 .load_preset(comfy_table::presets::UTF8_FULL)
13 .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
14 .set_header([
15 Cell::new("Repo"),
16 Cell::new("Worktree"),
17 Cell::new("Status"),
18 Cell::new("Branches"),
19 Cell::new("HEAD"),
20 Cell::new("Remotes"),
21 ]);
22}
23
24fn add_repo_status(
25 table: &mut Table,
26 repo_name: &str,
27 repo_handle: &repo::RepoHandle,
28 is_worktree: bool,
29) -> Result<(), String> {
30 let repo_status = repo_handle.status(is_worktree)?;
31
32 table.add_row([
33 repo_name,
34 if is_worktree { "\u{2714}" } else { "" },
35 &if is_worktree {
36 String::new()
37 } else {
38 match repo_status.changes {
39 Some(changes) => {
40 let mut out = Vec::new();
41 if changes.files_new > 0 {
42 out.push(format!("New: {}\n", changes.files_new));
43 }
44 if changes.files_modified > 0 {
45 out.push(format!("Modified: {}\n", changes.files_modified));
46 }
47 if changes.files_deleted > 0 {
48 out.push(format!("Deleted: {}\n", changes.files_deleted));
49 }
50 out.into_iter().collect::<String>().trim().to_string()
51 }
52 None => String::from("\u{2714}"),
53 }
54 },
55 repo_status
56 .branches
57 .iter()
58 .fold(String::new(), |mut s, (branch_name, remote_branch)| {
59 writeln!(
60 &mut s,
61 "branch: {}{}",
62 &branch_name,
63 &match remote_branch {
64 None => String::from(" <!local>"),
65 Some((remote_branch_name, remote_tracking_status)) => {
66 format!(
67 " <{}>{}",
68 remote_branch_name,
69 &match remote_tracking_status {
70 repo::RemoteTrackingStatus::UpToDate =>
71 String::from(" \u{2714}"),
72 repo::RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
73 repo::RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
74 repo::RemoteTrackingStatus::Diverged(d1, d2) =>
75 format!(" [+{}/-{}]", &d1, &d2),
76 }
77 )
78 }
79 }
80 )
81 .unwrap();
82 s
83 })
84 .trim(),
85 &if is_worktree {
86 String::new()
87 } else {
88 match repo_status.head {
89 Some(head) => head,
90 None => String::from("Empty"),
91 }
92 },
93 repo_status
94 .remotes
95 .iter()
96 .fold(String::new(), |mut s, r| {
97 writeln!(&mut s, "{r}").unwrap();
98 s
99 })
100 .trim(),
101 ]);
102
103 Ok(())
104}
105
106pub fn get_worktree_status_table(
108 repo: &repo::RepoHandle,
109 directory: &Path,
110) -> Result<(impl std::fmt::Display, Vec<String>), String> {
111 let worktrees = repo.get_worktrees()?;
112 let mut table = Table::new();
113
114 let mut errors = Vec::new();
115
116 add_worktree_table_header(&mut table);
117 for worktree in &worktrees {
118 let worktree_dir = &directory.join(worktree.name());
119 if worktree_dir.exists() {
120 let repo = match repo::RepoHandle::open(worktree_dir, false) {
121 Ok(repo) => repo,
122 Err(error) => {
123 errors.push(format!(
124 "Failed opening repo of worktree {}: {}",
125 &worktree.name(),
126 &error
127 ));
128 continue;
129 }
130 };
131 if let Err(error) = add_worktree_status(&mut table, worktree, &repo) {
132 errors.push(error);
133 }
134 } else {
135 errors.push(format!(
136 "Worktree {} does not have a directory",
137 &worktree.name()
138 ));
139 }
140 }
141 for worktree in repo::RepoHandle::find_unmanaged_worktrees(repo, directory)? {
142 errors.push(format!(
143 "Found {}, which is not a valid worktree directory!",
144 &worktree
145 ));
146 }
147 Ok((table, errors))
148}
149
150pub fn get_status_table(config: config::Config) -> Result<(Vec<Table>, Vec<String>), String> {
151 let mut errors = Vec::new();
152 let mut tables = Vec::new();
153 for tree in config.trees()? {
154 let repos = tree.repos.unwrap_or_default();
155
156 let root_path = path::expand_path(Path::new(&tree.root));
157
158 let mut table = Table::new();
159 add_table_header(&mut table);
160
161 for repo in &repos {
162 let repo_path = root_path.join(&repo.name);
163
164 if !repo_path.exists() {
165 errors.push(format!(
166 "{}: Repository does not exist. Run sync?",
167 &repo.name
168 ));
169 continue;
170 }
171
172 let repo_handle = repo::RepoHandle::open(&repo_path, repo.worktree_setup);
173
174 let repo_handle = match repo_handle {
175 Ok(repo) => repo,
176 Err(error) => {
177 if error.kind == repo::RepoErrorKind::NotFound {
178 errors.push(format!(
179 "{}: No git repository found. Run sync?",
180 &repo.name
181 ));
182 } else {
183 errors.push(format!(
184 "{}: Opening repository failed: {}",
185 &repo.name, error
186 ));
187 }
188 continue;
189 }
190 };
191
192 if let Err(err) =
193 add_repo_status(&mut table, &repo.name, &repo_handle, repo.worktree_setup)
194 {
195 errors.push(format!("{}: Couldn't add repo status: {}", &repo.name, err));
196 }
197 }
198
199 tables.push(table);
200 }
201
202 Ok((tables, errors))
203}
204
205fn add_worktree_table_header(table: &mut Table) {
206 table
207 .load_preset(comfy_table::presets::UTF8_FULL)
208 .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
209 .set_header([
210 Cell::new("Worktree"),
211 Cell::new("Status"),
212 Cell::new("Branch"),
213 Cell::new("Remote branch"),
214 ]);
215}
216
217fn add_worktree_status(
218 table: &mut Table,
219 worktree: &repo::Worktree,
220 repo: &repo::RepoHandle,
221) -> Result<(), String> {
222 let repo_status = repo.status(false)?;
223
224 let local_branch = repo
225 .head_branch()
226 .map_err(|error| format!("Failed getting head branch: {error}"))?;
227
228 let upstream_output = match local_branch.upstream() {
229 Ok(remote_branch) => {
230 let remote_branch_name = remote_branch
231 .name()
232 .map_err(|error| format!("Failed getting name of remote branch: {error}"))?;
233
234 let (ahead, behind) = repo
235 .graph_ahead_behind(&local_branch, &remote_branch)
236 .map_err(|error| format!("Failed computing branch deviation: {error}"))?;
237
238 format!(
239 "{}{}\n",
240 &remote_branch_name,
241 &match (ahead, behind) {
242 (0, 0) => String::new(),
243 (d, 0) => format!(" [+{}]", &d),
244 (0, d) => format!(" [-{}]", &d),
245 (d1, d2) => format!(" [+{}/-{}]", &d1, &d2),
246 },
247 )
248 }
249 Err(_) => String::new(),
250 };
251
252 table.add_row([
253 worktree.name(),
254 &match repo_status.changes {
255 Some(changes) => {
256 let mut out = Vec::new();
257 if changes.files_new > 0 {
258 out.push(format!("New: {}\n", changes.files_new));
259 }
260 if changes.files_modified > 0 {
261 out.push(format!("Modified: {}\n", changes.files_modified));
262 }
263 if changes.files_deleted > 0 {
264 out.push(format!("Deleted: {}\n", changes.files_deleted));
265 }
266 out.into_iter().collect::<String>().trim().to_string()
267 }
268 None => String::from("\u{2714}"),
269 },
270 &local_branch
271 .name()
272 .map_err(|error| format!("Failed getting name of branch: {error}"))?,
273 &upstream_output,
274 ]);
275
276 Ok(())
277}
278
279pub fn show_single_repo_status(
280 path: &Path,
281) -> Result<(impl std::fmt::Display, Vec<String>), String> {
282 let mut table = Table::new();
283 let mut warnings = Vec::new();
284
285 let is_worktree = repo::RepoHandle::detect_worktree(path);
286 add_table_header(&mut table);
287
288 let repo_handle = repo::RepoHandle::open(path, is_worktree);
289
290 if let Err(error) = repo_handle {
291 return if error.kind == repo::RepoErrorKind::NotFound {
292 Err(String::from("Directory is not a git directory"))
293 } else {
294 Err(format!("Opening repository failed: {error}"))
295 };
296 };
297
298 let repo_name = match path.file_name() {
299 None => {
300 warnings.push(format!(
301 "Cannot detect repo name for path {}. Are you working in /?",
302 &path.display()
303 ));
304 String::from("unknown")
305 }
306 Some(file_name) => match file_name.to_str() {
307 None => {
308 warnings.push(format!(
309 "Name of repo directory {} is not valid UTF-8",
310 &path.display()
311 ));
312 String::from("invalid")
313 }
314 Some(name) => name.to_string(),
315 },
316 };
317
318 add_repo_status(&mut table, &repo_name, &repo_handle.unwrap(), is_worktree)?;
319
320 Ok((table, warnings))
321}