Skip to main content

grm/
table.rs

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
147// Don't return table, return a type that implements Display(?)
148pub 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}