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