vinxen-git 0.1.1

Git operations for Vinxen CLI
Documentation
use crate::Result;
use git2::{Repository, Status, StatusShow};
use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StatusKind {
    Modified,
    Added,
    Deleted,
    Renamed,
    Untracked,
    Ignored,
    Conflicted,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileStatus {
    pub path: String,
    pub kind: StatusKind,
    pub staged: bool,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RepoStatus {
    pub files: Vec<FileStatus>,
    pub staged_count: usize,
    pub modified_count: usize,
    pub untracked_count: usize,
}

impl RepoStatus {
    pub fn collect(repo: &Repository, _workdir: &Path) -> Result<Self> {
        let mut files = Vec::new();
        let mut staged_count = 0;
        let mut modified_count = 0;
        let mut untracked_count = 0;

        let statuses = repo.statuses(Some(
            git2::StatusOptions::new()
                .show(StatusShow::IndexAndWorkdir)
                .include_ignored(false)
                .recurse_untracked_dirs(false),
        ))?;

        for status in statuses.iter() {
            let path = status.path().unwrap_or("").to_string();
            let status_flags = status.status();

            let is_staged = status_flags.intersects(
                Status::INDEX_NEW
                    | Status::INDEX_MODIFIED
                    | Status::INDEX_DELETED
                    | Status::INDEX_RENAMED
                    | Status::INDEX_TYPECHANGE,
            );

            let kind = if status_flags.intersects(Status::CONFLICTED) {
                StatusKind::Conflicted
            } else if status_flags.intersects(Status::WT_NEW) {
                StatusKind::Untracked
            } else if status_flags.intersects(Status::WT_MODIFIED) {
                StatusKind::Modified
            } else if status_flags.intersects(Status::WT_DELETED) {
                StatusKind::Deleted
            } else if status_flags.intersects(Status::WT_RENAMED) {
                StatusKind::Renamed
            } else if status_flags.intersects(Status::INDEX_NEW) {
                StatusKind::Added
            } else if status_flags.intersects(Status::INDEX_MODIFIED) {
                StatusKind::Modified
            } else if status_flags.intersects(Status::INDEX_DELETED) {
                StatusKind::Deleted
            } else if status_flags.intersects(Status::INDEX_RENAMED) {
                StatusKind::Renamed
            } else if status_flags.intersects(Status::INDEX_TYPECHANGE) {
                StatusKind::Modified
            } else {
                continue;
            };

            if is_staged {
                staged_count += 1;
            }
            match kind {
                StatusKind::Modified
                | StatusKind::Deleted
                | StatusKind::Renamed
                | StatusKind::Conflicted => modified_count += 1,
                StatusKind::Added | StatusKind::Untracked => untracked_count += 1,
                StatusKind::Ignored => {}
            }

            files.push(FileStatus {
                path,
                kind,
                staged: is_staged,
            });
        }

        Ok(Self {
            files,
            staged_count,
            modified_count,
            untracked_count,
        })
    }

    pub fn is_clean(&self) -> bool {
        self.files.is_empty()
    }

    pub fn has_staged(&self) -> bool {
        self.staged_count > 0
    }

    pub fn has_unstaged(&self) -> bool {
        self.modified_count > 0 || self.untracked_count > 0
    }
}