starship 0.8.7

The cross-shell prompt for astronauts. โ˜„๐ŸŒŒ๏ธ
Documentation
use ansi_term::Color;
use git2::{Repository, Status};

use super::{Context, Module};

/// Creates a module with the Git branch in the current directory
///
/// Will display the branch name if the current directory is a git repo
/// By default, the following symbols will be used to represent the repo's status:
///   - `=` โ€“ This branch has merge conflicts
///   - `โ‡ก` โ€“ This branch is ahead of the branch being tracked
///   - `โ‡ฃ` โ€“ This branch is behind of the branch being tracked
///   - `โ‡•` โ€“ This branch has diverged from the branch being tracked
///   - `?` โ€” There are untracked files in the working directory
///   - `$` โ€” A stash exists for the local repository
///   - `!` โ€” There are file modifications in the working directory
///   - `+` โ€” A new file has been added to the staging area
///   - `ยป` โ€” A renamed file has been added to the staging area
///   - `โœ˜` โ€” A file's deletion has been added to the staging area
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
    // This is the order that the sections will appear in
    const GIT_STATUS_CONFLICTED: &str = "=";
    const GIT_STATUS_AHEAD: &str = "โ‡ก";
    const GIT_STATUS_BEHIND: &str = "โ‡ฃ";
    const GIT_STATUS_DIVERGED: &str = "โ‡•";
    const GIT_STATUS_UNTRACKED: &str = "?";
    const GIT_STATUS_STASHED: &str = "$";
    const GIT_STATUS_MODIFIED: &str = "!";
    const GIT_STATUS_ADDED: &str = "+";
    const GIT_STATUS_RENAMED: &str = "ยป";
    const GIT_STATUS_DELETED: &str = "โœ˜";

    let branch_name = context.branch_name.as_ref()?;
    let repo_root = context.repo_root.as_ref()?;
    let repository = Repository::open(repo_root).ok()?;

    let module_style = Color::Red.bold();
    let mut module = context.new_module("git_status")?;
    module.get_prefix().set_value("[").set_style(module_style);
    module.get_suffix().set_value("] ").set_style(module_style);
    module.set_style(module_style);

    let ahead_behind = get_ahead_behind(&repository, branch_name);
    if ahead_behind == Ok((0, 0)) {
        log::trace!("No ahead/behind found");
    } else {
        log::debug!("Repo ahead/behind: {:?}", ahead_behind);
    }

    let stash_object = repository.revparse_single("refs/stash");
    if stash_object.is_ok() {
        log::debug!("Stash object: {:?}", stash_object);
    } else {
        log::trace!("No stash object found");
    }

    let repo_status = get_repo_status(&repository);
    log::debug!("Repo status: {:?}", repo_status);

    // Add the conflicted segment
    if let Ok(repo_status) = repo_status {
        if repo_status.is_conflicted() {
            module.new_segment("conflicted", GIT_STATUS_CONFLICTED);
        }
    }

    // Add the ahead/behind segment
    if let Ok((ahead, behind)) = ahead_behind {
        if ahead > 0 && behind > 0 {
            module.new_segment("diverged", GIT_STATUS_DIVERGED);
        } else if ahead > 0 {
            module.new_segment("ahead", GIT_STATUS_AHEAD);
        } else if behind > 0 {
            module.new_segment("behind", GIT_STATUS_BEHIND);
        }
    }

    // Add the stashed segment
    if stash_object.is_ok() {
        module.new_segment("stashed", GIT_STATUS_STASHED);
    }

    // Add all remaining status segments
    if let Ok(repo_status) = repo_status {
        if repo_status.is_wt_deleted() || repo_status.is_index_deleted() {
            module.new_segment("deleted", GIT_STATUS_DELETED);
        }

        if repo_status.is_wt_renamed() || repo_status.is_index_renamed() {
            module.new_segment("renamed", GIT_STATUS_RENAMED);
        }

        if repo_status.is_wt_modified() {
            module.new_segment("modified", GIT_STATUS_MODIFIED);
        }

        if repo_status.is_index_modified() || repo_status.is_index_new() {
            module.new_segment("staged", GIT_STATUS_ADDED);
        }

        if repo_status.is_wt_new() {
            module.new_segment("untracked", GIT_STATUS_UNTRACKED);
        }
    }

    if module.is_empty() {
        None
    } else {
        Some(module)
    }
}

/// Gets the bitflags associated with the repo's git status
fn get_repo_status(repository: &Repository) -> Result<Status, git2::Error> {
    let mut status_options = git2::StatusOptions::new();
    status_options.include_untracked(true);

    let repo_file_statuses = repository.statuses(Some(&mut status_options))?;

    // Statuses are stored as bitflags, so use BitOr to join them all into a single value
    let repo_status: Status = repo_file_statuses.iter().map(|e| e.status()).collect();
    if repo_status.is_empty() {
        return Err(git2::Error::from_str("Repo has no status"));
    }

    Ok(repo_status)
}

/// Compares the current branch with the branch it is tracking to determine how
/// far ahead or behind it is in relation
fn get_ahead_behind(
    repository: &Repository,
    branch_name: &str,
) -> Result<(usize, usize), git2::Error> {
    let branch_object = repository.revparse_single(branch_name)?;
    let tracking_branch_name = format!("{}@{{upstream}}", branch_name);
    let tracking_object = repository.revparse_single(&tracking_branch_name)?;

    let branch_oid = branch_object.id();
    let tracking_oid = tracking_object.id();

    repository.graph_ahead_behind(branch_oid, tracking_oid)
}