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