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