git_repos/
table.rs

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