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('*') {
let rhs_empty = rhs_opt.is_some_and(|r| r.is_empty());
if (rhs_opt.is_some() && !is_glob && !rhs_empty)
|| (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()
}
fn translate_single_star(
from_template: &str,
into_template: &str,
from_ref: &str,
) -> Option<String> {
let (from_pre, from_post) = split_one_star(from_template)?;
let (into_pre, into_post) = split_one_star(into_template)?;
if !from_ref.starts_with(from_pre) || !from_ref.ends_with(from_post) {
return None;
}
let middle_len = from_ref
.len()
.checked_sub(from_pre.len() + from_post.len())?;
let middle = &from_ref[from_pre.len()..from_pre.len() + middle_len];
Some(format!("{into_pre}{middle}{into_post}"))
}
fn split_one_star(template: &str) -> Option<(&str, &str)> {
let star = template.find('*')?;
if template[star + 1..].contains('*') {
return None;
}
Some((&template[..star], &template[star + 1..]))
}
fn refname_matches_template(template: &str, is_pattern: bool, refname: &str) -> bool {
if is_pattern {
translate_single_star(template, template, refname).is_some()
} else {
template == refname
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FetchLocalBinding {
pub force: bool,
pub upstream_ref: String,
}
fn positive_local_binding(spec: &RefspecItem, local_ref: &str) -> Option<FetchLocalBinding> {
if spec.negative {
return None;
}
let dst = spec.dst.as_deref()?;
let src = spec.src.as_deref().unwrap_or("");
let upstream_ref = if spec.pattern {
translate_single_star(dst, src, local_ref)?
} else if dst == local_ref {
src.to_owned()
} else {
return None;
};
Some(FetchLocalBinding {
force: spec.force,
upstream_ref,
})
}
fn excluded_by_negative_specs(specs: &[RefspecItem], upstream_ref: &str) -> bool {
specs.iter().any(|spec| {
spec.negative
&& spec
.src
.as_deref()
.is_some_and(|src| refname_matches_template(src, spec.pattern, upstream_ref))
})
}
#[must_use]
pub(crate) fn fetch_specs_bind_local_ref(
specs: &[RefspecItem],
local_ref: &str,
) -> Option<FetchLocalBinding> {
let binding = specs
.iter()
.find_map(|spec| positive_local_binding(spec, local_ref))?;
if excluded_by_negative_specs(specs, &binding.upstream_ref) {
return None;
}
Some(binding)
}
#[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_valid("refs/heads/*:");
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");
}
#[test]
fn fetch_specs_bind_local_ref_maps_tracking_branch() {
let specs = vec![parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap()];
let binding = fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/main").unwrap();
assert_eq!(binding.upstream_ref, "refs/heads/main");
}
#[test]
fn fetch_specs_bind_local_ref_honors_negative_line() {
let specs = vec![
parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap(),
parse_fetch_refspec("^refs/heads/private").unwrap(),
];
assert!(fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/main").is_some());
assert!(fetch_specs_bind_local_ref(&specs, "refs/remotes/origin/private").is_none());
}
}