feat(git): replace git2 with git-repository (#3883)

This commit is contained in:
David Knaack 2022-08-09 04:33:00 +02:00 committed by GitHub
parent f614fcdc1f
commit ac55a01d0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 871 additions and 182 deletions

784
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -22,9 +22,8 @@ keywords = ["prompt", "shell", "bash", "fish", "zsh"]
license = "ISC" license = "ISC"
readme = "README.md" readme = "README.md"
repository = "https://github.com/starship/starship" repository = "https://github.com/starship/starship"
# MSRV is specified due to our dependency in git2
# Note: MSRV is only intended as a hint, and only the latest version is officially supported in starship. # Note: MSRV is only intended as a hint, and only the latest version is officially supported in starship.
rust-version = "1.60" rust-version = "1.59"
description = """ description = """
The minimal, blazing-fast, and infinitely customizable prompt for any shell! 🌌 The minimal, blazing-fast, and infinitely customizable prompt for any shell! 🌌
""" """
@ -43,7 +42,7 @@ clap_complete = "3.2.3"
dirs-next = "2.0.0" dirs-next = "2.0.0"
dunce = "1.0.2" dunce = "1.0.2"
gethostname = "0.2.3" gethostname = "0.2.3"
git2 = { version = "0.14.4", default-features = false } git-repository = "0.20.0"
indexmap = { version = "1.9.1", features = ["serde"] } indexmap = { version = "1.9.1", features = ["serde"] }
local_ipaddress = "0.1.3" local_ipaddress = "0.1.3"
log = { version = "0.4.16", features = ["std"] } log = { version = "0.4.16", features = ["std"] }
@ -65,7 +64,7 @@ semver = "1.0.13"
serde = { version = "1.0.142", features = ["derive"] } serde = { version = "1.0.142", features = ["derive"] }
serde_json = "1.0.83" serde_json = "1.0.83"
sha-1 = "0.10.0" sha-1 = "0.10.0"
shadow-rs = "0.16.1" shadow-rs = { version = "0.16.1", default-features = false }
# battery is optional (on by default) because the crate doesn't currently build for Termux # battery is optional (on by default) because the crate doesn't currently build for Termux
# see: https://github.com/svartalf/rust-battery/issues/33 # see: https://github.com/svartalf/rust-battery/issues/33
starship-battery = { version = "0.7.9", optional = true } starship-battery = { version = "0.7.9", optional = true }
@ -109,7 +108,7 @@ features = [
nix = { version = "0.24.2", default-features = false, features = ["feature", "fs", "user"] } nix = { version = "0.24.2", default-features = false, features = ["feature", "fs", "user"] }
[build-dependencies] [build-dependencies]
shadow-rs = "0.16.1" shadow-rs = { version = "0.16.1", default-features = false }
dunce = "1.0.2" dunce = "1.0.2"
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]

View File

@ -6,7 +6,11 @@ use crate::utils::{create_command, exec_timeout, read_file, CommandOutput};
use crate::modules; use crate::modules;
use crate::utils::{self, home_dir}; use crate::utils::{self, home_dir};
use clap::Parser; use clap::Parser;
use git2::{ErrorCode::UnbornBranch, Repository, RepositoryState}; use git_repository::{
self as git,
sec::{self as git_sec, trust::DefaultForLevel},
state as git_state, Repository, ThreadSafeRepository,
};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
#[cfg(test)] #[cfg(test)]
use std::collections::HashMap; use std::collections::HashMap;
@ -255,22 +259,51 @@ impl<'a> Context<'a> {
} }
/// Will lazily get repo root and branch when a module requests it. /// Will lazily get repo root and branch when a module requests it.
pub fn get_repo(&self) -> Result<&Repo, git2::Error> { pub fn get_repo(&self) -> Result<&Repo, git::discover::Error> {
self.repo.get_or_try_init(|| -> Result<Repo, git2::Error> { self.repo
let repository = if env::var("GIT_DIR").is_ok() { .get_or_try_init(|| -> Result<Repo, git::discover::Error> {
Repository::open_from_env() // custom open options
} else { let mut git_open_opts_map =
let dirs: [PathBuf; 0] = []; git_sec::trust::Mapping::<git::open::Options>::default();
Repository::open_ext(&self.current_dir, git2::RepositoryOpenFlags::FROM_ENV, dirs)
}?; // don't use the global git configs
Ok(Repo { let config = git::permissions::Config {
branch: get_current_branch(&repository), system: false,
workdir: repository.workdir().map(Path::to_path_buf), git: false,
path: Path::to_path_buf(repository.path()), user: false,
state: repository.state(), env: true,
remote: get_remote_repository_info(&repository), includes: true,
};
// change options for config permissions without touching anything else
git_open_opts_map.reduced =
git_open_opts_map.reduced.permissions(git::Permissions {
config,
..git::Permissions::default_for_level(git_sec::Trust::Reduced)
});
git_open_opts_map.full = git_open_opts_map.full.permissions(git::Permissions {
config,
..git::Permissions::default_for_level(git_sec::Trust::Full)
});
let shared_repo = ThreadSafeRepository::discover_with_environment_overrides_opts(
&self.current_dir,
Default::default(),
git_open_opts_map,
)?;
let repository = shared_repo.to_thread_local();
let branch = get_current_branch(&repository);
let remote = get_remote_repository_info(&repository, branch.as_deref());
let path = repository.path().to_path_buf();
Ok(Repo {
repo: shared_repo,
branch,
workdir: repository.work_dir().map(PathBuf::from),
path,
state: repository.state(),
remote,
})
}) })
})
} }
pub fn dir_contents(&self) -> Result<&DirContents, std::io::Error> { pub fn dir_contents(&self) -> Result<&DirContents, std::io::Error> {
@ -494,6 +527,8 @@ impl DirContents {
} }
pub struct Repo { pub struct Repo {
pub repo: ThreadSafeRepository,
/// If `current_dir` is a git repository or is contained within one, /// If `current_dir` is a git repository or is contained within one,
/// this is the current branch name of that repo. /// this is the current branch name of that repo.
pub branch: Option<String>, pub branch: Option<String>,
@ -506,7 +541,7 @@ pub struct Repo {
pub path: PathBuf, pub path: PathBuf,
/// State /// State
pub state: RepositoryState, pub state: Option<git_state::InProgress>,
/// Remote repository /// Remote repository
pub remote: Option<Remote>, pub remote: Option<Remote>,
@ -514,8 +549,8 @@ pub struct Repo {
impl Repo { impl Repo {
/// Opens the associated git repository. /// Opens the associated git repository.
pub fn open(&self) -> Result<Repository, git2::Error> { pub fn open(&self) -> Repository {
Repository::open(&self.path) self.repo.to_thread_local()
} }
} }
@ -570,54 +605,26 @@ impl<'a> ScanDir<'a> {
} }
fn get_current_branch(repository: &Repository) -> Option<String> { fn get_current_branch(repository: &Repository) -> Option<String> {
let head = match repository.head() { let name = repository.head_name().ok()??;
Ok(reference) => reference, let shorthand = name.shorten();
Err(e) => {
return if e.code() == UnbornBranch {
// HEAD should only be an unborn branch if the repository is fresh,
// in that case read directly from `.git/HEAD`
let mut head_path = repository.path().to_path_buf();
head_path.push("HEAD");
// get first line, then last path segment Some(shorthand.to_string())
fs::read_to_string(&head_path)
.ok()?
.lines()
.next()?
.trim()
.split('/')
.last()
.map(std::borrow::ToOwned::to_owned)
} else {
None
};
}
};
let shorthand = head.shorthand();
shorthand.map(std::string::ToString::to_string)
} }
fn get_remote_repository_info(repository: &Repository) -> Option<Remote> { fn get_remote_repository_info(
if let Ok(head) = repository.head() { repository: &Repository,
if let Some(local_branch_ref) = head.name() { branch_name: Option<&str>,
let remote_ref = match repository.branch_upstream_name(local_branch_ref) { ) -> Option<Remote> {
Ok(remote_ref) => remote_ref.as_str()?.to_owned(), let branch_name = branch_name?;
Err(_) => return None, let branch = repository
}; .remote_ref(branch_name)
.and_then(|r| r.ok())
.map(|r| r.shorten().to_string());
let name = repository
.branch_remote_name(branch_name)
.map(|n| n.to_string());
let mut v = remote_ref.splitn(4, '/'); Some(Remote { branch, name })
let remote_name = v.nth(2)?.to_owned();
let remote_branch = v.last()?.to_owned();
return Some(Remote {
branch: Some(remote_branch),
name: Some(remote_name),
});
}
}
None
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@ -112,12 +112,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
{ {
let root = repo_path_vec[0]; let root = repo_path_vec[0];
let before = dir_string.replace(&contracted_path, ""); let before = dir_string.replace(&contracted_path, "");
[prefix + &before, root.to_string(), after_repo_root] [prefix + before.as_str(), root.to_string(), after_repo_root]
} else { } else {
["".to_string(), "".to_string(), prefix + &dir_string] ["".to_string(), "".to_string(), prefix + dir_string.as_str()]
} }
} }
_ => ["".to_string(), "".to_string(), prefix + &dir_string], _ => ["".to_string(), "".to_string(), prefix + dir_string.as_str()],
}; };
let path_vec = if config.use_os_path_sep { let path_vec = if config.use_os_path_sep {

View File

@ -26,12 +26,8 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let repo = context.get_repo().ok()?; let repo = context.get_repo().ok()?;
if config.only_attached { if config.only_attached && repo.open().head().ok()?.is_detached() {
if let Ok(git_repo) = repo.open() { return None;
if git_repo.head_detached().ok()? {
return None;
}
}
} }
let branch_name = repo.branch.as_ref()?; let branch_name = repo.branch.as_ref()?;

View File

@ -1,8 +1,8 @@
use super::{Context, Module, ModuleConfig}; use super::{Context, Module, ModuleConfig};
use crate::formatter::string_formatter::StringFormatterError; use git_repository::commit::describe::SelectRef::AnnotatedTags;
use git2::Time;
use crate::configs::git_commit::GitCommitConfig; use crate::configs::git_commit::GitCommitConfig;
use crate::context::Repo;
use crate::formatter::StringFormatter; use crate::formatter::StringFormatter;
/// Creates a module with the Git commit in the current directory /// Creates a module with the Git commit in the current directory
@ -13,45 +13,14 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let config: GitCommitConfig = GitCommitConfig::try_load(module.config); let config: GitCommitConfig = GitCommitConfig::try_load(module.config);
let repo = context.get_repo().ok()?; let repo = context.get_repo().ok()?;
let git_repo = repo.open().ok()?; let git_repo = repo.open();
let git_head = git_repo.head().ok()?;
let is_detached = git_repo.head_detached().ok()?; let is_detached = git_head.is_detached();
if config.only_detached && !is_detached { if config.only_detached && !is_detached {
return None; return None;
}; };
let git_head = git_repo.head().ok()?;
let head_commit = git_head.peel_to_commit().ok()?;
let commit_oid = head_commit.id();
let mut tag_name = String::new();
if !config.tag_disabled {
// Let's get repo tags names
let tag_names = git_repo.tag_names(None).ok()?;
let tag_and_refs = tag_names.iter().flatten().flat_map(|name| {
let full_tag = format!("refs/tags/{}", name);
let tag_obj = git_repo.find_reference(&full_tag)?.peel_to_tag()?;
let sig_obj = tag_obj.tagger();
git_repo.find_reference(&full_tag).map(|reference| {
(
String::from(name),
// fall back to oldest + 1s time if sig_obj is unavailable
sig_obj.map_or(git2::Time::new(1, 0), |s| s.when()),
reference,
)
})
});
let mut oldest = Time::new(0, 0);
// Let's check if HEAD has some tag. If several, gets last created one...
for (name, timestamp, reference) in tag_and_refs.rev() {
if commit_oid == reference.peel_to_commit().ok()?.id() && timestamp > oldest {
tag_name = name;
oldest = timestamp;
}
}
};
let parsed = StringFormatter::new(config.format).and_then(|formatter| { let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter formatter
.map_style(|variable| match variable { .map_style(|variable| match variable {
@ -59,11 +28,12 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
_ => None, _ => None,
}) })
.map(|variable| match variable { .map(|variable| match variable {
"hash" => Some(Ok(id_to_hex_abbrev( "hash" => Some(Ok(git_hash(context.get_repo().ok()?, &config)?)),
commit_oid.as_bytes(), "tag" => Some(Ok(format!(
config.commit_hash_length, "{}{}",
config.tag_symbol,
git_tag(context.get_repo().ok()?)?
))), ))),
"tag" => format_tag(config.tag_symbol, &tag_name),
_ => None, _ => None,
}) })
.parse(None, Some(context)) .parse(None, Some(context))
@ -80,23 +50,24 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
Some(module) Some(module)
} }
fn format_tag(symbol: &str, tag_name: &str) -> Option<Result<String, StringFormatterError>> { fn git_tag(repo: &Repo) -> Option<String> {
if tag_name.is_empty() { let git_repo = repo.open();
None let head_commit = git_repo.head_commit().ok()?;
} else {
Some(Ok(format!("{}{}", &symbol, &tag_name))) let describe_platform = head_commit.describe().names(AnnotatedTags);
} let formatter = describe_platform.try_format().ok()??;
Some(formatter.name?.to_string())
} }
/// len specifies length of hex encoded string fn git_hash(repo: &Repo, config: &GitCommitConfig) -> Option<String> {
fn id_to_hex_abbrev(bytes: &[u8], len: usize) -> String { let git_repo = repo.open();
bytes let head_id = git_repo.head_id().ok()?;
.iter()
.map(|b| format!("{:02x}", b)) Some(format!(
.collect::<String>() "{}",
.chars() head_id.to_hex_with_len(config.commit_hash_length)
.take(len) ))
.collect()
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,4 +1,4 @@
use git2::RepositoryState; use git_repository::state::InProgress;
use std::path::PathBuf; use std::path::PathBuf;
use super::{Context, Module, ModuleConfig}; use super::{Context, Module, ModuleConfig};
@ -54,54 +54,53 @@ fn get_state_description<'a>(
repo: &'a Repo, repo: &'a Repo,
config: &GitStateConfig<'a>, config: &GitStateConfig<'a>,
) -> Option<StateDescription<'a>> { ) -> Option<StateDescription<'a>> {
match repo.state { match repo.state.as_ref()? {
RepositoryState::Clean => None, InProgress::Merge => Some(StateDescription {
RepositoryState::Merge => Some(StateDescription {
label: config.merge, label: config.merge,
current: None, current: None,
total: None, total: None,
}), }),
RepositoryState::Revert => Some(StateDescription { InProgress::Revert => Some(StateDescription {
label: config.revert, label: config.revert,
current: None, current: None,
total: None, total: None,
}), }),
RepositoryState::RevertSequence => Some(StateDescription { InProgress::RevertSequence => Some(StateDescription {
label: config.revert, label: config.revert,
current: None, current: None,
total: None, total: None,
}), }),
RepositoryState::CherryPick => Some(StateDescription { InProgress::CherryPick => Some(StateDescription {
label: config.cherry_pick, label: config.cherry_pick,
current: None, current: None,
total: None, total: None,
}), }),
RepositoryState::CherryPickSequence => Some(StateDescription { InProgress::CherryPickSequence => Some(StateDescription {
label: config.cherry_pick, label: config.cherry_pick,
current: None, current: None,
total: None, total: None,
}), }),
RepositoryState::Bisect => Some(StateDescription { InProgress::Bisect => Some(StateDescription {
label: config.bisect, label: config.bisect,
current: None, current: None,
total: None, total: None,
}), }),
RepositoryState::ApplyMailbox => Some(StateDescription { InProgress::ApplyMailbox => Some(StateDescription {
label: config.am, label: config.am,
current: None, current: None,
total: None, total: None,
}), }),
RepositoryState::ApplyMailboxOrRebase => Some(StateDescription { InProgress::ApplyMailboxRebase => Some(StateDescription {
label: config.am_or_rebase, label: config.am_or_rebase,
current: None, current: None,
total: None, total: None,
}), }),
RepositoryState::Rebase => Some(describe_rebase(repo, config.rebase)), InProgress::Rebase => Some(describe_rebase(repo, config.rebase)),
RepositoryState::RebaseInteractive => Some(describe_rebase(repo, config.rebase)), InProgress::RebaseInteractive => Some(describe_rebase(repo, config.rebase)),
RepositoryState::RebaseMerge => Some(describe_rebase(repo, config.rebase)),
} }
} }
// TODO: Use future gitoxide API to get the state of the rebase
fn describe_rebase<'a>(repo: &'a Repo, rebase_config: &'a str) -> StateDescription<'a> { fn describe_rebase<'a>(repo: &'a Repo, rebase_config: &'a str) -> StateDescription<'a> {
/* /*
* Sadly, libgit2 seems to have some issues with reading the state of * Sadly, libgit2 seems to have some issues with reading the state of

View File

@ -34,6 +34,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
//Return None if not in git repository //Return None if not in git repository
context.get_repo().ok()?; context.get_repo().ok()?;
if let Some(git_status) = git_status_wsl(context, &config) { if let Some(git_status) = git_status_wsl(context, &config) {
if git_status.is_empty() { if git_status.is_empty() {
return None; return None;

View File

@ -41,7 +41,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// The truncation symbol should only be added if we truncated // The truncation symbol should only be added if we truncated
let truncated_and_symbol = if len < graphemes_len(&branch_name) { let truncated_and_symbol = if len < graphemes_len(&branch_name) {
let truncation_symbol = get_graphemes(config.truncation_symbol, 1); let truncation_symbol = get_graphemes(config.truncation_symbol, 1);
truncated_graphemes + &truncation_symbol truncated_graphemes + truncation_symbol.as_str()
} else { } else {
truncated_graphemes truncated_graphemes
}; };

View File

@ -150,7 +150,7 @@ mod tests {
.collect(); .collect();
let expected = Some(format!( let expected = Some(format!(
"{} in ", "{} in ",
style().paint("🌐 ".to_owned() + &hostname) style().paint("🌐 ".to_owned() + hostname.as_str())
)); ));
assert_eq!(expected, actual); assert_eq!(expected, actual);