Skip to content

Commit

Permalink
perf(git_status): replace git2 in git status module with git cli (sta…
Browse files Browse the repository at this point in the history
  • Loading branch information
dodgez authored Mar 23, 2021
1 parent 88c3844 commit 0a091bd
Showing 1 changed file with 83 additions and 101 deletions.
184 changes: 83 additions & 101 deletions src/modules/git_status.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use git2::{Repository, Status};
use once_cell::sync::OnceCell;
use regex::Regex;

use super::{Context, Module, RootModuleConfig};

use crate::configs::git_status::GitStatusConfig;
use crate::context::Repo;
use crate::formatter::StringFormatter;
use crate::segment::Segment;
use std::path::PathBuf;
use std::sync::Arc;

const ALL_STATUS_FORMAT: &str = "$conflicted$stashed$deleted$renamed$modified$staged$untracked";
Expand All @@ -27,7 +28,7 @@ const ALL_STATUS_FORMAT: &str = "$conflicted$stashed$deleted$renamed$modified$st
/// - `✘` — A file's deletion has been added to the staging area
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let repo = context.get_repo().ok()?;
let info = Arc::new(GitStatusInfo::load(repo));
let info = Arc::new(GitStatusInfo::load(context, repo));

let mut module = context.new_module("git_status");
let config: GitStatusConfig = GitStatusConfig::try_load(module.config);
Expand Down Expand Up @@ -108,59 +109,34 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
}

struct GitStatusInfo<'a> {
context: &'a Context<'a>,
repo: &'a Repo,
ahead_behind: OnceCell<Option<(usize, usize)>>,
repo_status: OnceCell<Option<RepoStatus>>,
stashed_count: OnceCell<Option<usize>>,
}

impl<'a> GitStatusInfo<'a> {
pub fn load(repo: &'a Repo) -> Self {
pub fn load(context: &'a Context, repo: &'a Repo) -> Self {
Self {
context,
repo,
ahead_behind: OnceCell::new(),
repo_status: OnceCell::new(),
stashed_count: OnceCell::new(),
}
}

fn get_branch_name(&self) -> String {
self.repo
.branch
.clone()
.unwrap_or_else(|| String::from("master"))
}

fn get_repository(&self) -> Option<Repository> {
// bare repos don't have a branch name, so `repo.branch.as_ref` would return None,
// but git treats "master" as the default branch name
let repo_root = self.repo.root.as_ref()?;
Repository::open(repo_root).ok()
}

pub fn get_ahead_behind(&self) -> &Option<(usize, usize)> {
self.ahead_behind.get_or_init(|| {
let repo = self.get_repository()?;
let branch_name = self.get_branch_name();

match get_ahead_behind(&repo, &branch_name) {
Ok(ahead_behind) => Some(ahead_behind),
Err(error) => {
log::debug!("get_ahead_behind: {}", error);
None
}
}
})
pub fn get_ahead_behind(&self) -> Option<(usize, usize)> {
self.get_repo_status().map(|data| (data.ahead, data.behind))
}

pub fn get_repo_status(&self) -> &Option<RepoStatus> {
self.repo_status.get_or_init(|| {
let mut repo = self.get_repository()?;
let repo_root = self.repo.root.as_ref()?;

match get_repo_status(&mut repo) {
Ok(repo_status) => Some(repo_status),
Err(error) => {
log::debug!("get_repo_status: {}", error);
match get_repo_status(self.context, repo_root) {
Some(repo_status) => Some(repo_status),
None => {
log::debug!("get_repo_status: git status execution failed");
None
}
}
Expand All @@ -169,12 +145,12 @@ impl<'a> GitStatusInfo<'a> {

pub fn get_stashed(&self) -> &Option<usize> {
self.stashed_count.get_or_init(|| {
let mut repo = self.get_repository()?;
let repo_root = self.repo.root.as_ref()?;

match get_stashed_count(&mut repo) {
Ok(stashed_count) => Some(stashed_count),
Err(error) => {
log::debug!("get_stashed_count: {}", error);
match get_stashed_count(self.context, repo_root) {
Some(stashed_count) => Some(stashed_count),
None => {
log::debug!("get_stashed_count: git stash execution failed");
None
}
}
Expand Down Expand Up @@ -207,62 +183,53 @@ impl<'a> GitStatusInfo<'a> {
}

/// Gets the number of files in various git states (staged, modified, deleted, etc...)
fn get_repo_status(repository: &mut Repository) -> Result<RepoStatus, git2::Error> {
fn get_repo_status(context: &Context, repo_root: &PathBuf) -> Option<RepoStatus> {
log::debug!("New repo status created");
let mut status_options = git2::StatusOptions::new();

let mut repo_status = RepoStatus::default();
let status_output = context.exec_cmd(
"git",
&[
"-C",
&repo_root.to_string_lossy(),
"--no-optional-locks",
"status",
"--porcelain=2",
"--branch",
],
)?;
let statuses = status_output.stdout.lines();

statuses.for_each(|status| {
if status.starts_with("# branch.ab ") {
repo_status.set_ahead_behind(status);
} else if !status.starts_with('#') {
repo_status.add(status);
}
});

match repository.config()?.get_entry("status.showUntrackedFiles") {
Ok(entry) => status_options.include_untracked(entry.value() != Some("no")),
_ => status_options.include_untracked(true),
};
status_options
.renames_from_rewrites(true)
.renames_head_to_index(true)
.include_unmodified(true);

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

if statuses.is_empty() {
return Err(git2::Error::from_str("Repo has no status"));
}

statuses
.iter()
.map(|s| s.status())
.for_each(|status| repo_status.add(status));

Ok(repo_status)
}

fn get_stashed_count(repository: &mut Repository) -> Result<usize, git2::Error> {
let mut count = 0;
repository.stash_foreach(|_, _, _| {
count += 1;
true
})?;
Result::Ok(count)
Some(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)
fn get_stashed_count(context: &Context, repo_root: &PathBuf) -> Option<usize> {
let stash_output = context.exec_cmd(
"git",
&[
"-C",
&repo_root.to_string_lossy(),
"--no-optional-locks",
"stash",
"list",
],
)?;

Some(stash_output.stdout.trim().lines().count())
}

#[derive(Default, Debug, Copy, Clone)]
struct RepoStatus {
ahead: usize,
behind: usize,
conflicted: usize,
deleted: usize,
renamed: usize,
Expand All @@ -272,38 +239,53 @@ struct RepoStatus {
}

impl RepoStatus {
fn is_conflicted(status: Status) -> bool {
status.is_conflicted()
fn is_conflicted(status: &str) -> bool {
status.starts_with("u ")
}

fn is_deleted(status: Status) -> bool {
status.is_wt_deleted() || status.is_index_deleted()
fn is_deleted(status: &str) -> bool {
// is_wt_deleted || is_index_deleted
status.starts_with("1 .D") || status.starts_with("1 D")
}

fn is_renamed(status: Status) -> bool {
status.is_wt_renamed() || status.is_index_renamed()
fn is_renamed(status: &str) -> bool {
// is_wt_renamed || is_index_renamed
// Potentially a copy and not a rename
status.starts_with("2 ")
}

fn is_modified(status: Status) -> bool {
status.is_wt_modified()
fn is_modified(status: &str) -> bool {
// is_wt_modified
status.starts_with("1 .M")
}

fn is_staged(status: Status) -> bool {
status.is_index_modified() || status.is_index_new()
fn is_staged(status: &str) -> bool {
// is_index_modified || is_index_new
status.starts_with("1 M") || status.starts_with("1 A")
}

fn is_untracked(status: Status) -> bool {
status.is_wt_new()
fn is_untracked(status: &str) -> bool {
// is_wt_new
status.starts_with("? ")
}

fn add(&mut self, s: Status) {
fn add(&mut self, s: &str) {
self.conflicted += RepoStatus::is_conflicted(s) as usize;
self.deleted += RepoStatus::is_deleted(s) as usize;
self.renamed += RepoStatus::is_renamed(s) as usize;
self.modified += RepoStatus::is_modified(s) as usize;
self.staged += RepoStatus::is_staged(s) as usize;
self.untracked += RepoStatus::is_untracked(s) as usize;
}

fn set_ahead_behind(&mut self, s: &str) {
let re = Regex::new(r"branch\.ab \+([0-9]+) \-([0-9]+)").unwrap();

if let Some(caps) = re.captures(s) {
self.ahead = caps.get(1).unwrap().as_str().parse::<usize>().unwrap();
self.behind = caps.get(2).unwrap().as_str().parse::<usize>().unwrap();
}
}
}

fn format_text<F>(format_str: &str, config_path: &str, mapper: F) -> Option<Vec<Segment>>
Expand Down

0 comments on commit 0a091bd

Please sign in to comment.