grm/
table.rs

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