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