From 0a091bd236421cc36a8aba015915c28cdb4d0323 Mon Sep 17 00:00:00 2001 From: Zachary Dodge <5016575+dodgez@users.noreply.github.com> Date: Tue, 23 Mar 2021 09:45:27 -0700 Subject: [PATCH] perf(git_status): replace git2 in git status module with git cli (#2465) --- src/modules/git_status.rs | 184 +++++++++++++++++--------------------- 1 file changed, 83 insertions(+), 101 deletions(-) diff --git a/src/modules/git_status.rs b/src/modules/git_status.rs index ea8f7e76464f..e773cdfb52e3 100644 --- a/src/modules/git_status.rs +++ b/src/modules/git_status.rs @@ -1,5 +1,5 @@ -use git2::{Repository, Status}; use once_cell::sync::OnceCell; +use regex::Regex; use super::{Context, Module, RootModuleConfig}; @@ -7,6 +7,7 @@ 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"; @@ -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> { 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); @@ -108,59 +109,34 @@ pub fn module<'a>(context: &'a Context) -> Option> { } struct GitStatusInfo<'a> { + context: &'a Context<'a>, repo: &'a Repo, - ahead_behind: OnceCell>, repo_status: OnceCell>, stashed_count: OnceCell>, } 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 { - // 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 { 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 } } @@ -169,12 +145,12 @@ impl<'a> GitStatusInfo<'a> { pub fn get_stashed(&self) -> &Option { 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 } } @@ -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 { +fn get_repo_status(context: &Context, repo_root: &PathBuf) -> Option { 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 { - 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 { + 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, @@ -272,31 +239,37 @@ 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; @@ -304,6 +277,15 @@ impl RepoStatus { 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::().unwrap(); + self.behind = caps.get(2).unwrap().as_str().parse::().unwrap(); + } + } } fn format_text(format_str: &str, config_path: &str, mapper: F) -> Option>