perf(git_status): replace git2 in git status module with git cli (#2465)

This commit is contained in:
Zachary Dodge 2021-03-23 09:45:27 -07:00 committed by GitHub
parent 88c3844db3
commit 0a091bd236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 81 additions and 99 deletions

View File

@ -1,5 +1,5 @@
use git2::{Repository, Status};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use regex::Regex;
use super::{Context, Module, RootModuleConfig}; use super::{Context, Module, RootModuleConfig};
@ -7,6 +7,7 @@ use crate::configs::git_status::GitStatusConfig;
use crate::context::Repo; use crate::context::Repo;
use crate::formatter::StringFormatter; use crate::formatter::StringFormatter;
use crate::segment::Segment; use crate::segment::Segment;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
const ALL_STATUS_FORMAT: &str = "$conflicted$stashed$deleted$renamed$modified$staged$untracked"; const ALL_STATUS_FORMAT: &str = "$conflicted$stashed$deleted$renamed$modified$staged$untracked";
@ -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 /// - `✘` — A file's deletion has been added to the staging area
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let repo = context.get_repo().ok()?; 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 mut module = context.new_module("git_status");
let config: GitStatusConfig = GitStatusConfig::try_load(module.config); let config: GitStatusConfig = GitStatusConfig::try_load(module.config);
@ -108,59 +109,34 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
} }
struct GitStatusInfo<'a> { struct GitStatusInfo<'a> {
context: &'a Context<'a>,
repo: &'a Repo, repo: &'a Repo,
ahead_behind: OnceCell<Option<(usize, usize)>>,
repo_status: OnceCell<Option<RepoStatus>>, repo_status: OnceCell<Option<RepoStatus>>,
stashed_count: OnceCell<Option<usize>>, stashed_count: OnceCell<Option<usize>>,
} }
impl<'a> GitStatusInfo<'a> { impl<'a> GitStatusInfo<'a> {
pub fn load(repo: &'a Repo) -> Self { pub fn load(context: &'a Context, repo: &'a Repo) -> Self {
Self { Self {
context,
repo, repo,
ahead_behind: OnceCell::new(),
repo_status: OnceCell::new(), repo_status: OnceCell::new(),
stashed_count: OnceCell::new(), stashed_count: OnceCell::new(),
} }
} }
fn get_branch_name(&self) -> String { pub fn get_ahead_behind(&self) -> Option<(usize, usize)> {
self.repo self.get_repo_status().map(|data| (data.ahead, data.behind))
.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_repo_status(&self) -> &Option<RepoStatus> { pub fn get_repo_status(&self) -> &Option<RepoStatus> {
self.repo_status.get_or_init(|| { 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) { match get_repo_status(self.context, repo_root) {
Ok(repo_status) => Some(repo_status), Some(repo_status) => Some(repo_status),
Err(error) => { None => {
log::debug!("get_repo_status: {}", error); log::debug!("get_repo_status: git status execution failed");
None None
} }
} }
@ -169,12 +145,12 @@ impl<'a> GitStatusInfo<'a> {
pub fn get_stashed(&self) -> &Option<usize> { pub fn get_stashed(&self) -> &Option<usize> {
self.stashed_count.get_or_init(|| { 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) { match get_stashed_count(self.context, repo_root) {
Ok(stashed_count) => Some(stashed_count), Some(stashed_count) => Some(stashed_count),
Err(error) => { None => {
log::debug!("get_stashed_count: {}", error); log::debug!("get_stashed_count: git stash execution failed");
None None
} }
} }
@ -207,62 +183,53 @@ impl<'a> GitStatusInfo<'a> {
} }
/// Gets the number of files in various git states (staged, modified, deleted, etc...) /// 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"); log::debug!("New repo status created");
let mut status_options = git2::StatusOptions::new();
let mut repo_status = RepoStatus::default(); 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();
match repository.config()?.get_entry("status.showUntrackedFiles") { statuses.for_each(|status| {
Ok(entry) => status_options.include_untracked(entry.value() != Some("no")), if status.starts_with("# branch.ab ") {
_ => status_options.include_untracked(true), repo_status.set_ahead_behind(status);
}; } else if !status.starts_with('#') {
status_options repo_status.add(status);
.renames_from_rewrites(true) }
.renames_head_to_index(true) });
.include_unmodified(true);
let statuses = repository.statuses(Some(&mut status_options))?; Some(repo_status)
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> { fn get_stashed_count(context: &Context, repo_root: &PathBuf) -> Option<usize> {
let mut count = 0; let stash_output = context.exec_cmd(
repository.stash_foreach(|_, _, _| { "git",
count += 1; &[
true "-C",
})?; &repo_root.to_string_lossy(),
Result::Ok(count) "--no-optional-locks",
} "stash",
"list",
],
)?;
/// Compares the current branch with the branch it is tracking to determine how Some(stash_output.stdout.trim().lines().count())
/// 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)
} }
#[derive(Default, Debug, Copy, Clone)] #[derive(Default, Debug, Copy, Clone)]
struct RepoStatus { struct RepoStatus {
ahead: usize,
behind: usize,
conflicted: usize, conflicted: usize,
deleted: usize, deleted: usize,
renamed: usize, renamed: usize,
@ -272,31 +239,37 @@ struct RepoStatus {
} }
impl RepoStatus { impl RepoStatus {
fn is_conflicted(status: Status) -> bool { fn is_conflicted(status: &str) -> bool {
status.is_conflicted() status.starts_with("u ")
} }
fn is_deleted(status: Status) -> bool { fn is_deleted(status: &str) -> bool {
status.is_wt_deleted() || status.is_index_deleted() // is_wt_deleted || is_index_deleted
status.starts_with("1 .D") || status.starts_with("1 D")
} }
fn is_renamed(status: Status) -> bool { fn is_renamed(status: &str) -> bool {
status.is_wt_renamed() || status.is_index_renamed() // is_wt_renamed || is_index_renamed
// Potentially a copy and not a rename
status.starts_with("2 ")
} }
fn is_modified(status: Status) -> bool { fn is_modified(status: &str) -> bool {
status.is_wt_modified() // is_wt_modified
status.starts_with("1 .M")
} }
fn is_staged(status: Status) -> bool { fn is_staged(status: &str) -> bool {
status.is_index_modified() || status.is_index_new() // is_index_modified || is_index_new
status.starts_with("1 M") || status.starts_with("1 A")
} }
fn is_untracked(status: Status) -> bool { fn is_untracked(status: &str) -> bool {
status.is_wt_new() // 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.conflicted += RepoStatus::is_conflicted(s) as usize;
self.deleted += RepoStatus::is_deleted(s) as usize; self.deleted += RepoStatus::is_deleted(s) as usize;
self.renamed += RepoStatus::is_renamed(s) as usize; self.renamed += RepoStatus::is_renamed(s) as usize;
@ -304,6 +277,15 @@ impl RepoStatus {
self.staged += RepoStatus::is_staged(s) as usize; self.staged += RepoStatus::is_staged(s) as usize;
self.untracked += RepoStatus::is_untracked(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>> fn format_text<F>(format_str: &str, config_path: &str, mapper: F) -> Option<Vec<Segment>>