use super::git::{Git, Unchecked};
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Spec {
pub remote: Option<String>,
pub sources: Vec<String>,
pub branches: bool,
pub tags: bool,
pub deletes: bool,
}
const VALUED: &[&str] = &["-o", "--push-option", "--repo", "--receive-pack", "--exec"];
pub fn spec(argv: &[String]) -> Spec {
let mut spec = Spec::default();
if argv.get(1).map(String::as_str) != Some("push") {
return spec;
}
let mut positional = Vec::new();
let mut words = argv[2..].iter();
let mut options = true;
while let Some(word) = words.next() {
if options && word == "--" {
options = false;
} else if options && word.starts_with('-') {
match word.as_str() {
"--all" | "--branches" => spec.branches = true,
"--mirror" => (spec.branches, spec.tags) = (true, true),
"--tags" => spec.tags = true,
"-d" | "--delete" => spec.deletes = true,
"--repo" => spec.remote = words.next().cloned(),
w if VALUED.contains(&w) => {
words.next();
}
w => {
if let Some(repo) = w.strip_prefix("--repo=") {
spec.remote = Some(repo.to_owned());
}
}
}
} else {
positional.push(word);
}
}
let mut positional = positional.into_iter();
if let Some(remote) = positional.next() {
spec.remote = Some(remote.clone());
}
for refspec in positional {
if spec.deletes {
break;
}
let refspec = refspec.strip_prefix('+').unwrap_or(refspec);
let source = refspec.split_once(':').map_or(refspec, |(src, _)| src);
if source.is_empty() {
continue;
}
if source.contains('*') {
spec.branches = true;
} else {
spec.sources.push(source.to_owned());
}
}
spec
}
pub fn revisions(git: &Git, spec: &Spec) -> Result<Option<Vec<String>>, Unchecked> {
if spec.deletes || git.value(&["rev-parse", "--git-dir"])?.is_none() {
return Ok(None);
}
let named = !spec.sources.is_empty() || spec.branches || spec.tags;
let mut revs = Vec::new();
if spec.branches {
revs.push("--branches".to_owned());
}
if spec.tags {
revs.push("--tags".to_owned());
}
if named {
for source in &spec.sources {
if source.starts_with('-') {
return Err(Unchecked(format!("`{source}` is not a refspec git reads")));
}
let Some(id) = commit(git, source)? else {
return Err(Unchecked(format!(
"which commit `{source}` names is not known"
)));
};
revs.push(id);
}
} else {
let Some(head) = commit(git, "HEAD")? else {
return Ok(None);
};
revs.push(head);
}
revs.push("--not".to_owned());
if !named && let Some(upstream) = commit(git, "@{u}")? {
revs.push(upstream);
return Ok(Some(revs));
}
let remote = match &spec.remote {
Some(remote) => remote.clone(),
None => branch_remote(git)?,
};
let known = git
.value(&["config", "--get", &format!("remote.{remote}.url")])?
.is_some();
if !known {
revs.push("--remotes".to_owned());
} else if let Some(default) = commit(git, &format!("refs/remotes/{remote}/HEAD"))? {
revs.push(default);
} else {
revs.push(format!("--remotes={remote}"));
}
Ok(Some(revs))
}
fn branch_remote(git: &Git) -> Result<String, Unchecked> {
let branch = git.value(&["symbolic-ref", "--quiet", "--short", "HEAD"])?;
let mut keys = Vec::new();
if let Some(b) = &branch {
keys.push(format!("branch.{b}.pushRemote"));
}
keys.push("remote.pushDefault".to_owned());
if let Some(b) = &branch {
keys.push(format!("branch.{b}.remote"));
}
for key in keys {
if let Some(remote) = git.value(&["config", "--get", &key])? {
return Ok(remote);
}
}
Ok("origin".to_owned())
}
fn commit(git: &Git, name: &str) -> Result<Option<String>, Unchecked> {
git.value(&[
"rev-parse",
"--verify",
"--quiet",
"--end-of-options",
&format!("{name}^{{commit}}"),
])
}
#[cfg(test)]
mod tests;