use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SparseCheckoutSet {
pub patterns: Vec<String>,
pub cone: bool,
}
impl SparseCheckoutSet {
pub fn new(patterns: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
patterns: patterns.into_iter().map(Into::into).collect(),
cone: true,
}
}
pub fn non_cone(mut self) -> Self {
self.cone = false;
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct WorktreeAdd {
pub path: PathBuf,
pub new_branch: Option<RefName>,
pub commitish: Option<RevSpec>,
pub no_checkout: bool,
}
impl WorktreeAdd {
pub fn checkout(path: impl Into<PathBuf>, commitish: RevSpec) -> Self {
Self {
path: path.into(),
new_branch: None,
commitish: Some(commitish),
no_checkout: false,
}
}
pub fn create_branch(path: impl Into<PathBuf>, name: RefName, commitish: RevSpec) -> Self {
Self {
path: path.into(),
new_branch: Some(name),
commitish: Some(commitish),
no_checkout: false,
}
}
pub fn no_checkout(mut self) -> Self {
self.no_checkout = true;
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct GitPush {
pub remote: String,
pub refspec: String,
pub set_upstream: bool,
}
impl GitPush {
pub fn branch(name: RefName) -> Self {
Self {
remote: "origin".to_string(),
refspec: name.as_str().to_string(),
set_upstream: false,
}
}
pub fn refspec(local: &RefName, remote_branch: &RefName) -> Self {
Self {
remote: "origin".to_string(),
refspec: format!("{}:{}", local.as_str(), remote_branch.as_str()),
set_upstream: false,
}
}
pub fn remote(mut self, remote: impl Into<String>) -> Self {
self.remote = remote.into();
self
}
pub fn set_upstream(mut self) -> Self {
self.set_upstream = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CloneFilter {
BlobNone,
TreeZero,
}
impl CloneFilter {
pub(crate) fn cli_value(self) -> &'static str {
match self {
Self::BlobNone => "blob:none",
Self::TreeZero => "tree:0",
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct CloneSpec {
pub branch: Option<String>,
pub depth: Option<u32>,
pub filter: Option<CloneFilter>,
pub single_branch: bool,
pub origin: Option<String>,
pub bare: bool,
}
impl CloneSpec {
pub fn new() -> Self {
Self::default()
}
pub fn branch(mut self, branch: impl Into<String>) -> Self {
self.branch = Some(branch.into());
self
}
pub fn depth(mut self, depth: u32) -> Self {
self.depth = Some(depth);
self
}
pub fn filter(mut self, filter: CloneFilter) -> Self {
self.filter = Some(filter);
self
}
pub fn single_branch(mut self) -> Self {
self.single_branch = true;
self
}
pub fn origin(mut self, name: impl Into<String>) -> Self {
self.origin = Some(name.into());
self
}
pub fn bare(mut self) -> Self {
self.bare = true;
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CommitPaths {
pub paths: Vec<PathBuf>,
pub message: String,
pub amend: bool,
}
impl CommitPaths {
pub fn new(
paths: impl IntoIterator<Item = impl Into<PathBuf>>,
message: impl Into<String>,
) -> Self {
Self {
paths: paths.into_iter().map(Into::into).collect(),
message: message.into(),
amend: false,
}
}
pub fn amend(mut self) -> Self {
self.amend = true;
self
}
}
#[derive(Debug, Clone)]
pub struct MergeCheckPartial {
branch: RefName,
}
impl MergeCheckPartial {
pub fn into_base(self, base: RevSpec) -> MergeCheck {
MergeCheck {
branch: self.branch,
base,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct MergeCheck {
pub branch: RefName,
pub base: RevSpec,
}
impl MergeCheck {
pub fn branch(name: RefName) -> MergeCheckPartial {
MergeCheckPartial { branch: name }
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MergeCommit {
pub branch: RevSpec,
pub no_ff: bool,
pub message: Option<String>,
}
impl MergeCommit {
pub fn branch(target: RevSpec) -> Self {
Self {
branch: target,
no_ff: false,
message: None,
}
}
pub fn no_ff(mut self) -> Self {
self.no_ff = true;
self
}
pub fn message(mut self, m: impl Into<String>) -> Self {
self.message = Some(m.into());
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MergeNoCommit {
pub branch: RevSpec,
pub squash: bool,
pub no_ff: bool,
}
impl MergeNoCommit {
pub fn branch(target: RevSpec) -> Self {
Self {
branch: target,
squash: false,
no_ff: false,
}
}
pub fn squash(mut self) -> Self {
self.squash = true;
self
}
pub fn no_ff(mut self) -> Self {
self.no_ff = true;
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AnnotatedTag {
pub name: RefName,
pub message: String,
pub rev: Option<RevSpec>,
}
impl AnnotatedTag {
pub fn new(name: RefName, message: impl Into<String>) -> Self {
Self {
name,
message: message.into(),
rev: None,
}
}
pub fn rev(mut self, r: RevSpec) -> Self {
self.rev = Some(r);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct BranchDelete {
pub name: RefName,
pub force: bool,
}
impl BranchDelete {
pub fn new(name: RefName) -> Self {
Self { name, force: false }
}
pub fn force(mut self) -> Self {
self.force = true;
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct StashPush {
pub include_untracked: bool,
}
impl StashPush {
pub fn new() -> Self {
Self::default()
}
pub fn include_untracked(mut self) -> Self {
self.include_untracked = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum CleanIgnored {
#[default]
Exclude,
Include,
Only,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct Clean {
pub force: bool,
pub dry_run: bool,
pub directories: bool,
pub ignored: CleanIgnored,
}
impl Clean {
pub fn new() -> Self {
Self::default()
}
pub fn force(mut self) -> Self {
self.force = true;
self
}
pub fn dry_run(mut self) -> Self {
self.dry_run = true;
self
}
pub fn directories(mut self) -> Self {
self.directories = true;
self
}
pub fn include_ignored(mut self) -> Self {
self.ignored = CleanIgnored::Include;
self
}
pub fn only_ignored(mut self) -> Self {
self.ignored = CleanIgnored::Only;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct WorktreeRemove {
pub path: PathBuf,
pub force: bool,
}
impl WorktreeRemove {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
force: false,
}
}
pub fn force(mut self) -> Self {
self.force = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct SubmoduleUpdate {
pub init: bool,
pub recursive: bool,
pub depth: Option<u32>,
pub paths: Vec<String>,
}
impl SubmoduleUpdate {
pub fn new() -> Self {
Self::default()
}
pub fn init(mut self) -> Self {
self.init = true;
self
}
pub fn recursive(mut self) -> Self {
self.recursive = true;
self
}
pub fn depth(mut self, depth: u32) -> Self {
self.depth = Some(depth);
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.paths.push(path.into());
self
}
pub fn paths(mut self, paths: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.paths.extend(paths.into_iter().map(Into::into));
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RefName(String);
impl RefName {
pub fn new(name: impl Into<String>) -> Result<Self> {
let name = name.into();
let bad = name.is_empty()
|| name.starts_with('-')
|| name.starts_with('.')
|| name.ends_with('/')
|| name.ends_with('.')
|| name.ends_with(".lock")
|| name.split('/').any(|component| {
component.is_empty()
|| component.starts_with('.')
|| component.ends_with('.')
|| component.ends_with(".lock")
})
|| name.contains("..")
|| name.contains("@{")
|| name == "@"
|| name
.chars()
.any(|c| c.is_control() || " ~^:?*[\\".contains(c));
if bad {
return Err(Error::spawn(
BINARY,
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid git reference name: {name:?}"),
),
));
}
Ok(RefName(name))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for RefName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RevSpec(String);
impl RevSpec {
pub fn new(rev: impl Into<String>) -> Result<Self> {
let rev = rev.into();
reject_flag_like("revision", &rev)?;
Ok(RevSpec(rev))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for RevSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for RefName {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
impl std::str::FromStr for RevSpec {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
#[cfg(test)]
mod tests {
use super::{RefName, RevSpec};
fn assert_invalid_ref_name(name: &str) {
let err = RefName::new(name).expect_err(&format!("{name:?} must be rejected"));
assert!(
vcs_cli_support::is_invalid_input(&err),
"{name:?} must classify as invalid input: {err:?}"
);
}
#[test]
fn ref_name_rejects_git_check_ref_format_boundaries() {
for name in [
"",
"-feature",
".feature",
"feature/",
"feature.lock",
"feature..name",
"feature name",
"feature~name",
"feature^name",
"feature:name",
"feature?name",
"feature*name",
"feature[name",
"feature\\name",
"feature\0name",
"feature.",
"feature@{upstream}",
"@",
] {
assert_invalid_ref_name(name);
}
}
#[test]
fn ref_name_rejects_invalid_slash_separated_components() {
for name in [
"/feature",
"feature//name",
"feature/.hidden",
"feature/name.",
"feature.lock/name",
"feature/name.lock",
] {
assert_invalid_ref_name(name);
}
}
#[test]
fn ref_name_accepts_valid_names_without_widening_revspec() {
for name in ["main", "feature/login", "release/v1.2.3", "feature@review"] {
assert_eq!(RefName::new(name).unwrap().as_str(), name);
}
for rev in ["main..feature", "feature@{upstream}", "@"] {
assert_eq!(RevSpec::new(rev).unwrap().as_str(), rev);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum BisectStep {
NextCandidate {
revision: RevSpec,
},
FirstBad {
revision: RevSpec,
},
}
impl BisectStep {
pub fn revision(&self) -> &RevSpec {
match self {
Self::NextCandidate { revision } | Self::FirstBad { revision } => revision,
}
}
pub fn is_first_bad(&self) -> bool {
matches!(self, Self::FirstBad { .. })
}
}
pub type BisectResult = BisectStep;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckoutTarget {
Ref(RevSpec),
Previous,
}
impl CheckoutTarget {
pub fn rev(rev: RevSpec) -> Self {
Self::Ref(rev)
}
pub fn previous() -> Self {
Self::Previous
}
pub(super) fn as_arg(&self) -> &str {
match self {
Self::Ref(rev) => rev.as_str(),
Self::Previous => "-",
}
}
}
impl From<RevSpec> for CheckoutTarget {
fn from(rev: RevSpec) -> Self {
Self::Ref(rev)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct GitCapabilities {
pub version: GitVersion,
}
const MIN_SUPPORTED_MAJOR: u64 = 2;
const MIN_SUPPORTED_MINOR: u64 = 31;
impl GitCapabilities {
pub fn is_supported(&self) -> bool {
(self.version.major, self.version.minor) >= (MIN_SUPPORTED_MAJOR, MIN_SUPPORTED_MINOR)
}
pub fn ensure_supported(&self) -> Result<()> {
if self.is_supported() {
return Ok(());
}
Err(Error::spawn(
BINARY,
std::io::Error::new(
std::io::ErrorKind::Unsupported,
format!(
"vcs-git requires git >= {MIN_SUPPORTED_MAJOR}.{MIN_SUPPORTED_MINOR} \
(validated on 2.54), found {}",
self.version
),
),
))
}
}