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_handle: &RepoHandle,
61 worktree_setup: WorktreeSetup,
62) -> Result<(), Error> {
63 let repo_status = repo_handle.status(worktree_setup).map_err(Error::Repo)?;
64
65 let branch_info = {
66 let mut acc = String::new();
67 for (branch_name, remote_branch) in repo_status.branches {
68 writeln!(
69 &mut acc,
70 "branch: {}{}",
71 branch_name,
72 match remote_branch {
73 None => String::from(" <!local>"),
74 Some((remote_branch_name, remote_tracking_status)) => {
75 format!(
76 " <{}>{}",
77 remote_branch_name,
78 match remote_tracking_status {
79 repo::RemoteTrackingStatus::UpToDate => String::from(" \u{2714}"),
80 repo::RemoteTrackingStatus::Ahead(d) => format!(" [+{d}]"),
81 repo::RemoteTrackingStatus::Behind(d) => format!(" [-{d}]"),
82 repo::RemoteTrackingStatus::Diverged(d1, d2) =>
83 format!(" [+{d1}/-{d2}]"),
84 }
85 )
86 }
87 }
88 )?;
89 }
90 acc.trim().to_owned()
91 };
92
93 let remote_status = {
94 let mut acc = String::new();
95 for remote in repo_status.remotes {
96 writeln!(&mut acc, "{remote}")?;
97 }
98
99 acc.trim().to_owned()
100 };
101
102 table.add_row([
103 match repo_name {
104 Some(name) => name.as_str(),
105 None => "unknown",
106 },
107 if worktree_setup.is_worktree() {
108 "\u{2714}"
109 } else {
110 ""
111 },
112 &if worktree_setup.is_worktree() {
113 String::new()
114 } else {
115 match repo_status.changes {
116 Some(changes) => {
117 let mut out = Vec::new();
118 if changes.files_new > 0 {
119 out.push(format!("New: {}\n", changes.files_new));
120 }
121 if changes.files_modified > 0 {
122 out.push(format!("Modified: {}\n", changes.files_modified));
123 }
124 if changes.files_deleted > 0 {
125 out.push(format!("Deleted: {}\n", changes.files_deleted));
126 }
127 out.into_iter().collect::<String>().trim().to_owned()
128 }
129 None => String::from("\u{2714}"),
130 }
131 },
132 &branch_info,
133 &if worktree_setup.is_worktree() {
134 String::new()
135 } else {
136 match repo_status.head {
137 Some(head) => head.into_string(),
138 None => String::from("Empty"),
139 }
140 },
141 &remote_status,
142 ]);
143
144 Ok(())
145}
146
147pub fn get_worktree_status_table(
149 repo: &WorktreeRepoHandle,
150 directory: &Path,
151) -> Result<(impl std::fmt::Display, Vec<Error>), Error> {
152 let worktrees = repo.get_worktrees()?;
153 let mut table = Table::new();
154
155 let mut errors = Vec::new();
156
157 add_worktree_table_header(&mut table);
158 for worktree in &worktrees {
159 let worktree_dir = &directory.join(worktree.name().as_str());
160 if worktree_dir.exists() {
161 let repo = match RepoHandle::open(worktree_dir) {
162 Ok(repo) => repo,
163 Err(error) => {
164 errors.push(error.into());
165 continue;
166 }
167 };
168 if let Err(error) = add_worktree_status(&mut table, worktree, &repo) {
169 errors.push(error);
170 }
171 } else {
172 errors.push(Error::WorktreeWithoutDirectory {
173 worktree: worktree.name().clone(),
174 });
175 }
176 }
177 for worktree in WorktreeRepoHandle::find_unmanaged_worktrees(repo, directory)? {
178 errors.push(Error::InvalidWorktreeDirectory { path: worktree });
179 }
180 Ok((table, errors))
181}
182
183pub fn get_status_table(trees: Vec<tree::Tree>) -> Result<(Vec<Table>, Vec<Error>), Error> {
184 let mut errors = Vec::new();
185 let mut tables = Vec::new();
186
187 for tree in trees {
188 let repos = tree.repos;
189
190 let root_path = path::expand_path(tree.root.as_path())?;
191
192 let mut table = Table::new();
193 add_table_header(&mut table);
194
195 for repo in &repos {
196 let repo_name = repo.fullname();
197 let repo_path = root_path.join(repo_name.as_str());
198
199 if !repo_path.exists() {
200 errors.push(Error::RepoDoesNotExist { name: repo_name });
201 continue;
202 }
203
204 let repo_handle = RepoHandle::open_with_worktree_setup(&repo_path, repo.worktree_setup);
205
206 let repo_handle = match repo_handle {
207 Ok(repo) => repo,
208 Err(error) => {
209 if matches!(error, repo::Error::RepoNotFound) {
210 errors.push(Error::RepoNotGit { name: repo_name });
211 } else {
212 errors.push(Error::RepoOpenFailed {
213 name: repo_name,
214 message: error.to_string(),
215 });
216 }
217 continue;
218 }
219 };
220
221 if let Err(err) = add_repo_status(
222 &mut table,
223 Some(&repo_name),
224 &repo_handle,
225 repo.worktree_setup,
226 ) {
227 errors.push(Error::RepoStatusFailed {
228 name: repo_name,
229 message: err.to_string(),
230 });
231 }
232 }
233
234 tables.push(table);
235 }
236
237 Ok((tables, errors))
238}
239
240fn add_worktree_table_header(table: &mut Table) {
241 table
242 .load_style(comfy_table::presets::UTF8_FULL.with_rounded_corners())
243 .set_header([
244 Cell::new("Worktree"),
245 Cell::new("Status"),
246 Cell::new("Branch"),
247 Cell::new("Remote branch"),
248 ]);
249}
250
251fn add_worktree_status(
252 table: &mut Table,
253 worktree: &repo::Worktree,
254 repo: &RepoHandle,
255) -> Result<(), Error> {
256 let repo_status = repo
257 .status(WorktreeSetup::NoWorktree)
258 .map_err(Error::Repo)?;
259
260 let local_branch = repo.head_branch().map_err(Error::Repo)?;
261
262 let upstream_output = match local_branch.upstream()? {
263 Some(remote_branch) => {
264 let remote_branch_name = remote_branch.name().map_err(Error::Repo)?;
265
266 let (ahead, behind) = repo
267 .graph_ahead_behind(&local_branch, &remote_branch)
268 .map_err(Error::Repo)?;
269
270 format!(
271 "{}{}\n",
272 remote_branch_name,
273 match (ahead, behind) {
274 (0, 0) => String::new(),
275 (d, 0) => format!(" [+{d}]"),
276 (0, d) => format!(" [-{d}]"),
277 (d1, d2) => format!(" [+{d1}/-{d2}]"),
278 },
279 )
280 }
281 None => String::new(),
282 };
283
284 table.add_row([
285 worktree.name().as_str(),
286 &match repo_status.changes {
287 Some(changes) => {
288 let mut out = Vec::new();
289 if changes.files_new > 0 {
290 out.push(format!("New: {}\n", changes.files_new));
291 }
292 if changes.files_modified > 0 {
293 out.push(format!("Modified: {}\n", changes.files_modified));
294 }
295 if changes.files_deleted > 0 {
296 out.push(format!("Deleted: {}\n", changes.files_deleted));
297 }
298 out.into_iter().collect::<String>().trim().to_owned()
299 }
300 None => String::from("\u{2714}"),
301 },
302 local_branch.name().map_err(Error::Repo)?.as_str(),
303 &upstream_output,
304 ]);
305
306 Ok(())
307}
308
309pub fn show_single_repo_status(
310 path: &Path,
311) -> Result<(impl std::fmt::Display, Vec<String>), Error> {
312 let mut table = Table::new();
313 let mut warnings = Vec::new();
314
315 let worktree_setup = WorktreeSetup::detect(path);
316 add_table_header(&mut table);
317
318 let repo_handle = RepoHandle::open_with_worktree_setup(path, worktree_setup);
319
320 if let Err(error) = repo_handle {
321 if matches!(error, repo::Error::RepoNotFound) {
322 return Err(Error::NotAGitDirectory);
323 } else {
324 return Err(error.into());
325 }
326 }
327
328 let repo_name = match path.file_name() {
329 None => {
330 warnings.push(format!(
331 "Cannot detect repo name for path {path}. Are you working in /?"
332 ));
333 None
334 }
335 Some(file_name) => Some(RepoName::new(file_name.to_owned())),
336 };
337
338 add_repo_status(
339 &mut table,
340 repo_name.as_ref(),
341 &repo_handle?,
342 worktree_setup,
343 )?;
344
345 Ok((table, warnings))
346}