use crate::check_ref_format::{check_refname_format, RefNameOptions};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RefspecItem {
pub force: bool,
pub negative: bool,
pub matching: bool,
pub pattern: bool,
pub exact_sha1: bool,
pub src: Option<String>,
pub dst: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefspecError {
Invalid(String),
}
impl std::fmt::Display for RefspecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RefspecError::Invalid(s) => write!(f, "invalid refspec '{s}'"),
}
}
}
impl std::error::Error for RefspecError {}
const SHA1_HEXSZ: usize = 40;
fn is_exact_sha1_hex(s: &str) -> bool {
s.len() == SHA1_HEXSZ && s.bytes().all(|b| b.is_ascii_hexdigit())
}
fn refname_ok(name: &str, is_glob: bool) -> bool {
let opts = RefNameOptions {
allow_onelevel: true,
refspec_pattern: is_glob,
normalize: false,
};
check_refname_format(name, &opts).is_ok()
}
fn parse_refspec(refspec: &str, fetch: bool) -> Result<RefspecItem, RefspecError> {
let bytes = refspec.as_bytes();
let invalid = || RefspecError::Invalid(refspec.to_owned());
let mut item = RefspecItem::default();
let mut is_glob = false;
let mut lhs_start = 0usize;
if let Some(&first) = bytes.first() {
if first == b'+' {
item.force = true;
lhs_start = 1;
} else if first == b'^' {
item.negative = true;
lhs_start = 1;
}
}
let lhs = &refspec[lhs_start..];
let colon_pos = lhs.rfind(':');
if item.negative && colon_pos.is_some() {
return Err(invalid());
}
if !fetch && colon_pos == Some(0) && lhs.len() == 1 {
item.matching = true;
return Ok(item);
}
let (lhs_str, rhs_opt): (&str, Option<&str>) = match colon_pos {
Some(pos) => (&lhs[..pos], Some(&lhs[pos + 1..])),
None => (lhs, None),
};
if let Some(rhs) = rhs_opt {
let rlen = rhs.len();
is_glob = rlen >= 1 && rhs.contains('*');
item.dst = Some(rhs.to_owned());
} else {
item.dst = None;
}
let llen = lhs_str.len();
if llen >= 1 && lhs_str.contains('*') {
if (rhs_opt.is_some() && !is_glob) || (rhs_opt.is_none() && !item.negative && fetch) {
return Err(invalid());
}
is_glob = true;
} else if rhs_opt.is_some() && is_glob {
return Err(invalid());
}
item.pattern = is_glob;
if llen == 1 && lhs_str == "@" {
item.src = Some("HEAD".to_owned());
} else {
item.src = Some(lhs_str.to_owned());
}
let src = item.src.as_deref().unwrap_or("");
if item.negative {
if src.is_empty() {
return Err(invalid()); } else if is_exact_sha1_hex(src) {
return Err(invalid()); } else if refname_ok(src, is_glob) {
} else {
return Err(invalid());
}
return Ok(item);
}
if fetch {
if src.is_empty() {
} else if is_exact_sha1_hex(src) {
item.exact_sha1 = true; } else if refname_ok(src, is_glob) {
} else {
return Err(invalid());
}
match item.dst.as_deref() {
None => {} Some("") => {} Some(dst) => {
if !refname_ok(dst, is_glob) {
return Err(invalid());
}
}
}
} else {
if src.is_empty() {
} else if is_glob {
if !refname_ok(src, is_glob) {
return Err(invalid());
}
} else {
}
match item.dst.as_deref() {
None => {
if !refname_ok(src, is_glob) {
return Err(invalid());
}
}
Some("") => {
return Err(invalid());
}
Some(dst) => {
if !refname_ok(dst, is_glob) {
return Err(invalid());
}
}
}
}
Ok(item)
}
pub fn parse_fetch_refspec(refspec: &str) -> Result<RefspecItem, RefspecError> {
parse_refspec(refspec, true)
}
pub fn parse_push_refspec(refspec: &str) -> Result<RefspecItem, RefspecError> {
parse_refspec(refspec, false)
}
pub fn valid_fetch_refspec(refspec: &str) -> bool {
parse_refspec(refspec, true).is_ok()
}
pub fn valid_push_refspec(refspec: &str) -> bool {
parse_refspec(refspec, false).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn fetch_valid(s: &str) {
assert!(valid_fetch_refspec(s), "expected fetch '{s}' to be valid");
}
fn fetch_invalid(s: &str) {
assert!(
!valid_fetch_refspec(s),
"expected fetch '{s}' to be invalid"
);
}
fn push_valid(s: &str) {
assert!(valid_push_refspec(s), "expected push '{s}' to be valid");
}
fn push_invalid(s: &str) {
assert!(!valid_push_refspec(s), "expected push '{s}' to be invalid");
}
#[test]
fn empty_and_colon() {
push_invalid("");
push_valid(":");
push_invalid("::");
push_valid("+:");
fetch_valid("");
fetch_valid(":");
fetch_invalid("::");
}
#[test]
fn glob_balance() {
push_valid("refs/heads/*:refs/remotes/frotz/*");
push_invalid("refs/heads/*:refs/remotes/frotz");
push_invalid("refs/heads:refs/remotes/frotz/*");
push_valid("refs/heads/main:refs/remotes/frotz/xyzzy");
fetch_valid("refs/heads/*:refs/remotes/frotz/*");
fetch_invalid("refs/heads/*:refs/remotes/frotz");
fetch_invalid("refs/heads:refs/remotes/frotz/*");
fetch_valid("refs/heads/main:refs/remotes/frotz/xyzzy");
fetch_invalid("refs/heads/main::refs/remotes/frotz/xyzzy");
fetch_invalid("refs/heads/maste :refs/remotes/frotz/xyzzy");
}
#[test]
fn rev_expressions() {
push_valid("main~1:refs/remotes/frotz/backup");
fetch_invalid("main~1:refs/remotes/frotz/backup");
push_valid("HEAD~4:refs/remotes/frotz/new");
fetch_invalid("HEAD~4:refs/remotes/frotz/new");
}
#[test]
fn bare_head_and_at() {
push_valid("HEAD");
fetch_valid("HEAD");
push_valid("@");
fetch_valid("@");
push_invalid("refs/heads/ nitfol");
fetch_invalid("refs/heads/ nitfol");
}
#[test]
fn head_colon() {
push_invalid("HEAD:");
fetch_valid("HEAD:");
push_invalid("refs/heads/ nitfol:");
fetch_invalid("refs/heads/ nitfol:");
}
#[test]
fn delete_specs() {
push_valid(":refs/remotes/frotz/deleteme");
fetch_valid(":refs/remotes/frotz/HEAD-to-me");
push_invalid(":refs/remotes/frotz/delete me");
fetch_invalid(":refs/remotes/frotz/HEAD to me");
}
#[test]
fn star_placements() {
fetch_valid("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
push_valid("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
fetch_valid("refs/heads*/for-linus:refs/remotes/mine/*");
push_valid("refs/heads*/for-linus:refs/remotes/mine/*");
fetch_invalid("refs/heads/*/*/for-linus:refs/remotes/mine/*");
push_invalid("refs/heads/*/*/for-linus:refs/remotes/mine/*");
fetch_invalid("refs/heads/*g*/for-linus:refs/remotes/mine/*");
push_invalid("refs/heads/*g*/for-linus:refs/remotes/mine/*");
fetch_valid("refs/heads/*/for-linus:refs/remotes/mine/*");
push_valid("refs/heads/*/for-linus:refs/remotes/mine/*");
}
#[test]
fn utf8_and_tab() {
fetch_valid("refs/heads/\u{00C4}");
fetch_invalid("refs/heads/\ttab");
}
}