1use std::fmt::{self, Write};
2
3use camino::{Utf8Path as Path, Utf8PathBuf as PathBuf};
4use comfy_table::{Cell, Table};
5use thiserror::Error;
6
7use super::{
8 config, path,
9 repo::{self, WorktreeName, WorktreeSetup},
10 repo::{RepoHandle, RepoName, WorktreeRepoHandle},
11 tree,
12};
13
14#[derive(Debug, Error)]
15pub enum Error {
16 #[error(transparent)]
17 Lib(#[from] crate::Error),
18 #[error(transparent)]
19 Config(#[from] config::Error),
20 #[error("Repo error: {0}")]
21 Repo(#[from] repo::Error),
22 #[error("Worktreeerror: {0}")]
23 Worktree(#[from] repo::WorktreeError),
24 #[error("Directory is not a git directory")]
25 NotAGitDirectory,
26 #[error("Worktree {worktree:?} does not have a directory")]
27 WorktreeWithoutDirectory { worktree: WorktreeName },
28 #[error(transparent)]
29 Path(#[from] path::Error),
30 #[error(transparent)]
31 Fmt(#[from] fmt::Error),
32 #[error("Found {path}, which is not a valid worktree directory!")]
33 InvalidWorktreeDirectory { path: PathBuf },
34 #[error("Repository \"{name}\" does not exist. Run sync?")]
35 RepoDoesNotExist { name: RepoName },
36 #[error("No git repository for \"{name}\" found. Run sync?")]
37 RepoNotGit { name: RepoName },
38 #[error("Opening repository \"{name}\" failed: {message}")]
39 RepoOpenFailed { name: RepoName, message: String },
40 #[error("Couldn't add repo status for \"{name}\": {message}")]
41 RepoStatusFailed { name: RepoName, message: String },
42}
43
44fn add_table_header(table: &mut Table) {
45 table
46 .load_style(comfy_table::presets::UTF8_FULL.with_rounded_corners())
47 .set_header([
48 Cell::new("Repo"),
49 Cell::new("Worktree"),
50 Cell::new("Status"),
51 Cell::new("Branches"),
52 Cell::new("HEAD"),
53 Cell::new("Remotes"),
54 ]);
55}
56
57fn add_repo_status(
58 table: &mut Table,
59 repo_name: Option<&RepoName>,
60 repo_status: repo::RepoStatus,
61 worktree_setup: WorktreeSetup,
62) -> Result<(), Error> {
63 let branch_info = {
64 let mut acc = String::new();
65 for (branch_name, remote_branch) in repo_status.branches {
66 writeln!(
67 &mut acc,
68 "branch: {}{}",
69 branch_name,
70 match remote_branch {
71 None => String::from(" <!local>"),
72 Some((remote_branch_name, remote_tracking_status)) => {
73 format!(
74 " <{}>{}",
75 remote_branch_name,
76 match remote_tracking_status {
77 repo::RemoteTrackingStatus::UpToDate => String::from(" \u{2714}"),
78 repo::RemoteTrackingStatus::Ahead(d) => format!(" [+{d}]"),
79 repo::RemoteTrackingStatus::Behind(d) => format!(" [-{d}]"),
80 repo::RemoteTrackingStatus::Diverged(d1, d2) =>
81 format!(" [+{d1}/-{d2}]"),
82 }
83 )
84 }
85 }
86 )?;
87 }
88 acc.trim().to_owned()
89 };
90
91 let remote_status = {
92 let mut acc = String::new();
93 for remote in repo_status.remotes {
94 writeln!(&mut acc, "{remote}")?;
95 }
96
97 acc.trim().to_owned()
98 };
99
100 table.add_row([
101 match repo_name {
102 Some(name) => name.as_str(),
103 None => "unknown",
104 },
105 if worktree_setup.is_worktree() {
106 "\u{2714}"
107 } else {
108 ""
109 },
110 &if worktree_setup.is_worktree() {
111 String::new()
112 } else {
113 match repo_status.changes {
114 Some(changes) => {
115 let mut out = Vec::new();
116 if changes.files_new > 0 {
117 out.push(format!("New: {}\n", changes.files_new));
118 }
119 if changes.files_modified > 0 {
120 out.push(format!("Modified: {}\n", changes.files_modified));
121 }
122 if changes.files_deleted > 0 {
123 out.push(format!("Deleted: {}\n", changes.files_deleted));
124 }
125 out.into_iter().collect::<String>().trim().to_owned()
126 }
127 None => String::from("\u{2714}"),
128 }
129 },
130 &branch_info,
131 &if worktree_setup.is_worktree() {
132 String::new()
133 } else {
134 match repo_status.head {
135 Some(head) => head.into_string(),
136 None => String::from("Empty"),
137 }
138 },
139 &remote_status,
140 ]);
141
142 Ok(())
143}
144
145pub fn get_worktree_status_table(
147 repo: &WorktreeRepoHandle,
148 directory: &Path,
149) -> Result<(impl std::fmt::Display, Vec<Error>), Error> {
150 let worktrees = repo.get_worktrees()?;
151 let mut table = Table::new();
152
153 let mut errors = Vec::new();
154
155 add_worktree_table_header(&mut table);
156 for worktree in &worktrees {
157 let worktree_dir = &directory.join(worktree.name().as_str());
158 if worktree_dir.exists() {
159 let repo = match RepoHandle::open(worktree_dir) {
160 Ok(repo) => repo,
161 Err(error) => {
162 errors.push(error.into());
163 continue;
164 }
165 };
166 if let Err(error) = add_worktree_status(&mut table, worktree, &repo) {
167 errors.push(error);
168 }
169 } else {
170 errors.push(Error::WorktreeWithoutDirectory {
171 worktree: worktree.name().clone(),
172 });
173 }
174 }
175 for worktree in WorktreeRepoHandle::find_unmanaged_worktrees(repo, directory)? {
176 errors.push(Error::InvalidWorktreeDirectory { path: worktree });
177 }
178 Ok((table, errors))
179}
180
181pub fn get_status_table(
182 trees: Vec<tree::Tree>,
183 dirty_only: bool,
184) -> Result<(Vec<Table>, Vec<Error>), Error> {
185 let mut errors = Vec::new();
186 let mut tables = Vec::new();
187
188 for tree in trees {
189 let repos = tree.repos;
190
191 let root_path = path::expand_path(tree.root.as_path())?;
192
193 let mut table = Table::new();
194 add_table_header(&mut table);
195 let mut rows: usize = 0;
196
197 for repo in &repos {
198 let repo_name = repo.fullname();
199 let repo_path = root_path.join(repo_name.as_str());
200
201 if !repo_path.exists() {
202 errors.push(Error::RepoDoesNotExist { name: repo_name });
203 continue;
204 }
205
206 let repo_handle = RepoHandle::open_with_worktree_setup(&repo_path, repo.worktree_setup);
207
208 let repo_handle = match repo_handle {
209 Ok(repo) => repo,
210 Err(error) => {
211 if matches!(error, repo::Error::RepoNotFound) {
212 errors.push(Error::RepoNotGit { name: repo_name });
213 } else {
214 errors.push(Error::RepoOpenFailed {
215 name: repo_name,
216 message: error.to_string(),
217 });
218 }
219 continue;
220 }
221 };
222
223 let repo_status = match repo_handle.status(repo.worktree_setup) {
224 Ok(repo_status) => repo_status,
225 Err(error) => {
226 errors.push(Error::RepoStatusFailed {
227 name: repo_name,
228 message: error.to_string(),
229 });
230 continue;
231 }
232 };
233
234 if dirty_only && !repo_status.dirty() {
235 continue;
236 }
237
238 match add_repo_status(
239 &mut table,
240 Some(&repo_name),
241 repo_status,
242 repo.worktree_setup,
243 ) {
244 Ok(()) => rows = rows.saturating_add(1),
245 Err(err) => errors.push(Error::RepoStatusFailed {
246 name: repo_name,
247 message: err.to_string(),
248 }),
249 }
250 }
251
252 if !dirty_only || rows > 0 {
255 tables.push(table);
256 }
257 }
258
259 Ok((tables, errors))
260}
261
262fn add_worktree_table_header(table: &mut Table) {
263 table
264 .load_style(comfy_table::presets::UTF8_FULL.with_rounded_corners())
265 .set_header([
266 Cell::new("Worktree"),
267 Cell::new("Status"),
268 Cell::new("Branch"),
269 Cell::new("Remote branch"),
270 ]);
271}
272
273fn add_worktree_status(
274 table: &mut Table,
275 worktree: &repo::Worktree,
276 repo: &RepoHandle,
277) -> Result<(), Error> {
278 let repo_status = repo
279 .status(WorktreeSetup::NoWorktree)
280 .map_err(Error::Repo)?;
281
282 let local_branch = repo.head_branch().map_err(Error::Repo)?;
283
284 let upstream_output = match local_branch.upstream()? {
285 Some(remote_branch) => {
286 let remote_branch_name = remote_branch.name().map_err(Error::Repo)?;
287
288 let (ahead, behind) = repo
289 .graph_ahead_behind(&local_branch, &remote_branch)
290 .map_err(Error::Repo)?;
291
292 format!(
293 "{}{}\n",
294 remote_branch_name,
295 match (ahead, behind) {
296 (0, 0) => String::new(),
297 (d, 0) => format!(" [+{d}]"),
298 (0, d) => format!(" [-{d}]"),
299 (d1, d2) => format!(" [+{d1}/-{d2}]"),
300 },
301 )
302 }
303 None => String::new(),
304 };
305
306 table.add_row([
307 worktree.name().as_str(),
308 &match repo_status.changes {
309 Some(changes) => {
310 let mut out = Vec::new();
311 if changes.files_new > 0 {
312 out.push(format!("New: {}\n", changes.files_new));
313 }
314 if changes.files_modified > 0 {
315 out.push(format!("Modified: {}\n", changes.files_modified));
316 }
317 if changes.files_deleted > 0 {
318 out.push(format!("Deleted: {}\n", changes.files_deleted));
319 }
320 out.into_iter().collect::<String>().trim().to_owned()
321 }
322 None => String::from("\u{2714}"),
323 },
324 local_branch.name().map_err(Error::Repo)?.as_str(),
325 &upstream_output,
326 ]);
327
328 Ok(())
329}
330
331pub fn show_single_repo_status(
332 path: &Path,
333 dirty_only: bool,
334) -> Result<(impl std::fmt::Display, Vec<String>), Error> {
335 let mut table = Table::new();
336 let mut warnings = Vec::new();
337
338 let worktree_setup = WorktreeSetup::detect(path);
339 add_table_header(&mut table);
340
341 let repo_handle = RepoHandle::open_with_worktree_setup(path, worktree_setup);
342
343 if let Err(error) = repo_handle {
344 if matches!(error, repo::Error::RepoNotFound) {
345 return Err(Error::NotAGitDirectory);
346 } else {
347 return Err(error.into());
348 }
349 }
350
351 let repo_name = match path.file_name() {
352 None => {
353 warnings.push(format!(
354 "Cannot detect repo name for path {path}. Are you working in /?"
355 ));
356 None
357 }
358 Some(file_name) => Some(RepoName::new(file_name.to_owned())),
359 };
360
361 let repo_status = repo_handle?.status(worktree_setup).map_err(Error::Repo)?;
362
363 if !dirty_only || repo_status.dirty() {
364 add_repo_status(&mut table, repo_name.as_ref(), repo_status, worktree_setup)?;
365 }
366
367 Ok((table, warnings))
368}