perf(git_status): replace git2 in git status module with git cli (#2465)
This commit is contained in:
parent
88c3844db3
commit
0a091bd236
|
@ -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>>
|
||||||
|
|
Loading…
Reference in New Issue