git-repo-manager 0.7.12

Manage multiple git repositories. You configure the git repositories in a file, the program does the rest!
Documentation
use super::config;
use super::path;
use super::repo;

use comfy_table::{Cell, Table};

use std::path::Path;

fn add_table_header(table: &mut Table) {
    table
        .load_preset(comfy_table::presets::UTF8_FULL)
        .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
        .set_header(vec![
            Cell::new("Repo"),
            Cell::new("Worktree"),
            Cell::new("Status"),
            Cell::new("Branches"),
            Cell::new("HEAD"),
            Cell::new("Remotes"),
        ]);
}

fn add_repo_status(
    table: &mut Table,
    repo_name: &str,
    repo_handle: &repo::RepoHandle,
    is_worktree: bool,
) -> Result<(), String> {
    let repo_status = repo_handle.status(is_worktree)?;

    table.add_row(vec![
        repo_name,
        match is_worktree {
            true => "\u{2714}",
            false => "",
        },
        &match is_worktree {
            true => String::from(""),
            false => match repo_status.changes {
                Some(changes) => {
                    let mut out = Vec::new();
                    if changes.files_new > 0 {
                        out.push(format!("New: {}\n", changes.files_new))
                    }
                    if changes.files_modified > 0 {
                        out.push(format!("Modified: {}\n", changes.files_modified))
                    }
                    if changes.files_deleted > 0 {
                        out.push(format!("Deleted: {}\n", changes.files_deleted))
                    }
                    out.into_iter().collect::<String>().trim().to_string()
                }
                None => String::from("\u{2714}"),
            },
        },
        repo_status
            .branches
            .iter()
            .map(|(branch_name, remote_branch)| {
                format!(
                    "branch: {}{}\n",
                    &branch_name,
                    &match remote_branch {
                        None => String::from(" <!local>"),
                        Some((remote_branch_name, remote_tracking_status)) => {
                            format!(
                                " <{}>{}",
                                remote_branch_name,
                                &match remote_tracking_status {
                                    repo::RemoteTrackingStatus::UpToDate =>
                                        String::from(" \u{2714}"),
                                    repo::RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
                                    repo::RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
                                    repo::RemoteTrackingStatus::Diverged(d1, d2) =>
                                        format!(" [+{}/-{}]", &d1, &d2),
                                }
                            )
                        }
                    }
                )
            })
            .collect::<String>()
            .trim(),
        &match is_worktree {
            true => String::from(""),
            false => match repo_status.head {
                Some(head) => head,
                None => String::from("Empty"),
            },
        },
        repo_status
            .remotes
            .iter()
            .map(|r| format!("{}\n", r))
            .collect::<String>()
            .trim(),
    ]);

    Ok(())
}

// Don't return table, return a type that implements Display(?)
pub fn get_worktree_status_table(
    repo: &repo::RepoHandle,
    directory: &Path,
) -> Result<(impl std::fmt::Display, Vec<String>), String> {
    let worktrees = repo.get_worktrees()?;
    let mut table = Table::new();

    let mut errors = Vec::new();

    add_worktree_table_header(&mut table);
    for worktree in &worktrees {
        let worktree_dir = &directory.join(worktree.name());
        if worktree_dir.exists() {
            let repo = match repo::RepoHandle::open(worktree_dir, false) {
                Ok(repo) => repo,
                Err(error) => {
                    errors.push(format!(
                        "Failed opening repo of worktree {}: {}",
                        &worktree.name(),
                        &error
                    ));
                    continue;
                }
            };
            if let Err(error) = add_worktree_status(&mut table, worktree, &repo) {
                errors.push(error);
            }
        } else {
            errors.push(format!(
                "Worktree {} does not have a directory",
                &worktree.name()
            ));
        }
    }
    for worktree in repo::RepoHandle::find_unmanaged_worktrees(repo, directory)? {
        errors.push(format!(
            "Found {}, which is not a valid worktree directory!",
            &worktree
        ));
    }
    Ok((table, errors))
}

pub fn get_status_table(config: config::Config) -> Result<(Vec<Table>, Vec<String>), String> {
    let mut errors = Vec::new();
    let mut tables = Vec::new();
    for tree in config.trees()? {
        let repos = tree.repos.unwrap_or_default();

        let root_path = path::expand_path(Path::new(&tree.root));

        let mut table = Table::new();
        add_table_header(&mut table);

        for repo in &repos {
            let repo_path = root_path.join(&repo.name);

            if !repo_path.exists() {
                errors.push(format!(
                    "{}: Repository does not exist. Run sync?",
                    &repo.name
                ));
                continue;
            }

            let repo_handle = repo::RepoHandle::open(&repo_path, repo.worktree_setup);

            let repo_handle = match repo_handle {
                Ok(repo) => repo,
                Err(error) => {
                    if error.kind == repo::RepoErrorKind::NotFound {
                        errors.push(format!(
                            "{}: No git repository found. Run sync?",
                            &repo.name
                        ));
                    } else {
                        errors.push(format!(
                            "{}: Opening repository failed: {}",
                            &repo.name, error
                        ));
                    }
                    continue;
                }
            };

            if let Err(err) =
                add_repo_status(&mut table, &repo.name, &repo_handle, repo.worktree_setup)
            {
                errors.push(format!("{}: Couldn't add repo status: {}", &repo.name, err));
            }
        }

        tables.push(table);
    }

    Ok((tables, errors))
}

fn add_worktree_table_header(table: &mut Table) {
    table
        .load_preset(comfy_table::presets::UTF8_FULL)
        .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
        .set_header(vec![
            Cell::new("Worktree"),
            Cell::new("Status"),
            Cell::new("Branch"),
            Cell::new("Remote branch"),
        ]);
}

fn add_worktree_status(
    table: &mut Table,
    worktree: &repo::Worktree,
    repo: &repo::RepoHandle,
) -> Result<(), String> {
    let repo_status = repo.status(false)?;

    let local_branch = repo
        .head_branch()
        .map_err(|error| format!("Failed getting head branch: {}", error))?;

    let upstream_output = match local_branch.upstream() {
        Ok(remote_branch) => {
            let remote_branch_name = remote_branch
                .name()
                .map_err(|error| format!("Failed getting name of remote branch: {}", error))?;

            let (ahead, behind) = repo
                .graph_ahead_behind(&local_branch, &remote_branch)
                .map_err(|error| format!("Failed computing branch deviation: {}", error))?;

            format!(
                "{}{}\n",
                &remote_branch_name,
                &match (ahead, behind) {
                    (0, 0) => String::from(""),
                    (d, 0) => format!(" [+{}]", &d),
                    (0, d) => format!(" [-{}]", &d),
                    (d1, d2) => format!(" [+{}/-{}]", &d1, &d2),
                },
            )
        }
        Err(_) => String::from(""),
    };

    table.add_row(vec![
        worktree.name(),
        &match repo_status.changes {
            Some(changes) => {
                let mut out = Vec::new();
                if changes.files_new > 0 {
                    out.push(format!("New: {}\n", changes.files_new))
                }
                if changes.files_modified > 0 {
                    out.push(format!("Modified: {}\n", changes.files_modified))
                }
                if changes.files_deleted > 0 {
                    out.push(format!("Deleted: {}\n", changes.files_deleted))
                }
                out.into_iter().collect::<String>().trim().to_string()
            }
            None => String::from("\u{2714}"),
        },
        &local_branch
            .name()
            .map_err(|error| format!("Failed getting name of branch: {}", error))?,
        &upstream_output,
    ]);

    Ok(())
}

pub fn show_single_repo_status(
    path: &Path,
) -> Result<(impl std::fmt::Display, Vec<String>), String> {
    let mut table = Table::new();
    let mut warnings = Vec::new();

    let is_worktree = repo::RepoHandle::detect_worktree(path);
    add_table_header(&mut table);

    let repo_handle = repo::RepoHandle::open(path, is_worktree);

    if let Err(error) = repo_handle {
        if error.kind == repo::RepoErrorKind::NotFound {
            return Err(String::from("Directory is not a git directory"));
        } else {
            return Err(format!("Opening repository failed: {}", error));
        }
    };

    let repo_name = match path.file_name() {
        None => {
            warnings.push(format!(
                "Cannot detect repo name for path {}. Are you working in /?",
                &path.display()
            ));
            String::from("unknown")
        }
        Some(file_name) => match file_name.to_str() {
            None => {
                warnings.push(format!(
                    "Name of repo directory {} is not valid UTF-8",
                    &path.display()
                ));
                String::from("invalid")
            }
            Some(name) => name.to_string(),
        },
    };

    add_repo_status(&mut table, &repo_name, &repo_handle.unwrap(), is_worktree)?;

    Ok((table, warnings))
}