-
-
Save kalkin/507cb07aa37b4c9c6fd2ed04706336c0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::iter::Peekable; | |
use git_repository::hash::Prefix; | |
use git_repository::prelude::*; | |
use git_repository::{Id, ObjectId, Repository}; | |
use crate::errors::RevParseError; | |
// refname: master, heads/master, refs/heads/master Default: HEAD | |
// rev: sha1|refname | |
// nav: revision navigation with '^' & '~' | |
// type: ^{commit} ^{blob} ^{tree} ^{tag} | |
// peel-tag: ^{} | |
// regex: /… | |
// spec: {type} | {peel-tag} | {\{regex\}} | |
// pure-regex: :/… | |
// path: :… | |
// {refname}@{REFLOG_SPEC} | |
// {refname}@{REMOTE_BRANCH_SPEC} | |
// {rev}{nav}*{spec}? | |
// {pure-regex} | |
fn parse_with_at<'repo>(repo: &'repo Repository, input: &str) -> Result<Id<'repo>, RevParseError> { | |
if input == "@" { | |
return Ok(repo.head_id()?); | |
} | |
if input.contains("@{") { | |
return Err(RevParseError(format!( | |
"NIY @{{ handling, needed for {}", | |
input | |
))); | |
} | |
Err(RevParseError("Parsing revisions with @ NIY".to_owned())) | |
} | |
fn split_parts(input: &str) -> Result<(String, String, String), RevParseError> { | |
#[derive(Copy, Clone, Debug)] | |
enum State { | |
Rev, | |
Nav, | |
Spec, | |
} | |
let mut rev = String::default(); | |
let mut nav = String::default(); | |
let mut spec = String::default(); | |
let mut state = State::Rev; | |
let mut it = input.chars().peekable(); | |
while let Some(c) = it.next() { | |
let next = match c { | |
'^' => match it.peek() { | |
Some('{') => State::Spec, | |
Some('0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '^' | '~') | |
| None => State::Nav, | |
_ => return Err(RevParseError("Invalid ref or id".to_owned())), | |
}, | |
'~' => State::Nav, | |
_ => state, | |
}; | |
match (state, next) { | |
// valid transitions | |
(State::Rev, State::Nav | State::Spec) | (State::Nav, State::Spec) => state = next, | |
// No transitions | |
(State::Rev, State::Rev) | (State::Nav, State::Nav) | (State::Spec, State::Spec) => {} | |
// Invalid | |
(State::Nav | State::Spec, State::Rev) | (State::Spec, State::Nav) => { | |
return Err(RevParseError(format!( | |
"Invalid transition {:?} / {:?}", | |
state, next | |
))) | |
} | |
} | |
match (state, next) { | |
// valid transitions | |
(State::Rev, State::Rev) => rev.push(c), | |
(State::Rev | State::Nav, State::Nav) => nav.push(c), | |
(State::Rev | State::Nav | State::Spec, State::Spec) => spec.push(c), | |
// Invalid | |
(State::Nav | State::Spec, State::Rev) | (State::Spec, State::Nav) => { | |
return Err(RevParseError(format!( | |
"Invalid transition {:?} / {:?}", | |
state, next | |
))) | |
} | |
} | |
} | |
Ok((rev, nav, spec)) | |
} | |
fn parse_rev_nav_spec<'repo>( | |
repo: &'repo Repository, | |
input: &str, | |
) -> Result<Id<'repo>, RevParseError> { | |
let (rev_str, nav_str, spec_str) = split_parts(input)?; | |
let rev = if rev_str.is_empty() { | |
Err(RevParseError("Empty input".to_owned())) | |
} else if rev_str == *"@" { | |
Ok(repo.head_id()?) | |
} else if let Ok(r) = repo.find_reference(&rev_str) { | |
Ok(r.into_fully_peeled_id()?) | |
} else if !is_hex(&rev_str) { | |
Err(RevParseError(format!("Invalid ref or id: {}", rev_str))) | |
} else if is_hex(&rev_str) && rev_str.len() == 40 { | |
// The function bellow does not what its named after | |
Ok(repo.rev_parse(rev_str)?) | |
} else if let Ok(object_id) = find_by_short_id(repo, &rev_str) { | |
Ok(object_id.attach(repo)) | |
} else { | |
Err(RevParseError(format!("Invalid ref or id: {}", rev_str))) | |
}?; | |
if nav_str.is_empty() && spec_str.is_empty() { | |
Ok(rev) | |
} else if !nav_str.is_empty() && spec_str.is_empty() { | |
Ok(navigate(&rev, &nav_str)?) | |
} else if nav_str.is_empty() && !spec_str.is_empty() { | |
Ok(apply_specials(repo, &rev, &spec_str)?) | |
} else { | |
let tmp = navigate(&rev, &nav_str)?; | |
Ok(apply_specials(repo, &tmp, &spec_str)?) | |
} | |
} | |
fn apply_specials<'repo>( | |
_repo: &'repo Repository, | |
_start: &Id<'_>, | |
_nav_str: &str, | |
) -> Result<Id<'repo>, RevParseError> { | |
Err(RevParseError("Specials in revision args NIY".to_owned())) | |
} | |
fn navigate<'repo>(start: &Id<'repo>, input: &str) -> Result<Id<'repo>, RevParseError> { | |
let mut cur = *start; | |
let mut it = input.chars().peekable(); | |
while let Some(c) = it.next() { | |
if c == '^' { | |
let length = parse_counter(&mut it); | |
let commit = cur.object().unwrap().try_into_commit().unwrap(); | |
cur = commit.parent_ids().take(length).last().unwrap(); | |
} else if c == '~' { | |
let length = parse_counter(&mut it); | |
let mut ancestors_it = cur.ancestors().first_parent_only().all().unwrap(); | |
ancestors_it.next(); | |
let ids: Result<Vec<_>, _> = ancestors_it.take(length).into_iter().collect(); | |
let f = ids.map_err(|e| RevParseError(format!("{}", e)))?; | |
if let Some(last) = f.last() { | |
cur = *last; | |
} else { | |
return Err(RevParseError(format!("History to short for {}", input))); | |
} | |
} else { | |
return Err(RevParseError(format!( | |
"Invalid revision navigation string: {}", | |
input | |
))); | |
} | |
} | |
Ok(cur) | |
} | |
fn parse_counter(it: &mut Peekable<std::str::Chars<'_>>) -> usize { | |
let mut buf = String::default(); | |
while let Some(next) = it.peek() { | |
if !next.is_digit(10) { | |
break; | |
} | |
buf.push(it.next().expect("Should be some")); | |
} | |
if buf.is_empty() { | |
1 | |
} else { | |
buf.parse().unwrap() | |
} | |
} | |
fn find_by_short_id(repo: &Repository, input: &str) -> Result<ObjectId, RevParseError> { | |
let padded_id = format!("{:0<40}", input); | |
let tmp_id = ObjectId::from_hex(padded_id.as_bytes()).unwrap(); | |
let prefix = Prefix::new(tmp_id, input.len()).unwrap(); | |
let r = repo | |
.objects | |
.lookup_prefix(prefix) | |
.map_err(|err| RevParseError(format!("{}", err)))? | |
.ok_or_else(|| RevParseError(format!("Short id {} not found", input)))?; | |
r.map_err(|_err| RevParseError(format!("Short id {} not found", input))) | |
} | |
fn search_by_regex<'_repo>( | |
_repo: &'_repo Repository, | |
input: &str, | |
) -> Result<Id<'_repo>, RevParseError> { | |
Err(RevParseError(format!( | |
"NIY Regex search, needed for {}", | |
input | |
))) | |
} | |
fn is_hex(input: &str) -> bool { | |
for c in input.chars() { | |
match c { | |
'a'..='f' | 'A'..='F' | '0'..='9' => continue, | |
_ => return false, | |
} | |
} | |
true | |
} | |
pub fn parse_revision<'repo>( | |
repo: &'repo Repository, | |
input: &str, | |
) -> Result<Id<'repo>, RevParseError> { | |
if input.contains("@{") { | |
return parse_with_at(repo, input); | |
} | |
if input.starts_with(":/") { | |
return search_by_regex(repo, input); | |
} | |
parse_rev_nav_spec(repo, input) | |
} | |
#[cfg(test)] | |
fn test_repo() -> Repository { | |
let cwd = std::env::current_dir().expect("CWD"); | |
git_repository::discover(cwd).expect("Repository") | |
} | |
#[cfg(test)] | |
mod simple { | |
use crate::gitrevisions::{parse_revision, test_repo}; | |
#[test] | |
fn full() { | |
let repo = test_repo(); | |
let expected = "dd613891fa47fc0cbfa813cd57fb63c038a08d62"; | |
let actual = &parse_revision(&repo, "dd613891fa47fc0cbfa813cd57fb63c038a08d62") | |
.expect("Found full SHA hash") | |
.to_hex() | |
.to_string(); | |
assert_eq!(actual, expected, "Parse full sha"); | |
} | |
#[test] | |
fn short() { | |
let repo = test_repo(); | |
let expected = "dd613891fa47fc0cbfa813cd57fb63c038a08d62"; | |
let actual = &parse_revision(&repo, "dd613891") | |
.expect("Found full 8 chars hash") | |
.to_hex() | |
.to_string(); | |
assert_eq!(actual, expected, "Parse full sha"); | |
} | |
#[test] | |
fn head() { | |
let repo = test_repo(); | |
let expected = repo.head_id().expect("Current repo has a HEAD"); | |
let actual = parse_revision(&repo, "HEAD").expect("Resolved HEAD"); | |
assert_eq!(actual, expected, "Parse HEAD"); | |
} | |
#[test] | |
fn at_sign() { | |
let repo = test_repo(); | |
let expected = repo.head_id().expect("Current repo has a HEAD"); | |
let actual = parse_revision(&repo, "@").expect("Resolved @"); | |
assert_eq!(actual, expected, "Parse HEAD"); | |
} | |
#[test] | |
fn master() { | |
let repo = test_repo(); | |
parse_revision(&repo, "master").expect("Resolved master branch"); | |
} | |
} | |
#[cfg(test)] | |
mod advanced { | |
use super::{parse_revision, test_repo}; | |
#[test] | |
fn at_previous() { | |
let repo = test_repo(); | |
let input = "@~"; | |
let expected = { | |
let head_id = repo.head_id().expect("Current repo has a HEAD"); | |
head_id | |
.object() | |
.unwrap() | |
.try_into_commit() | |
.unwrap() | |
.parent_ids() | |
.next() | |
.unwrap() | |
}; | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
#[test] | |
fn head_previous() { | |
let repo = test_repo(); | |
let input = "HEAD~"; | |
let expected = { | |
let head_id = repo.head_id().expect("Current repo has a HEAD"); | |
head_id | |
.object() | |
.unwrap() | |
.try_into_commit() | |
.unwrap() | |
.parent_ids() | |
.next() | |
.unwrap() | |
}; | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
#[test] | |
fn at_first_parent() { | |
let repo = test_repo(); | |
let input = "@^"; | |
let expected = { | |
let head_id = repo.head_id().expect("Current repo has a HEAD"); | |
head_id | |
.object() | |
.unwrap() | |
.try_into_commit() | |
.unwrap() | |
.parent_ids() | |
.next() | |
.unwrap() | |
}; | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
#[test] | |
fn head_first_parent() { | |
let repo = test_repo(); | |
let input = "HEAD^"; | |
let expected = { | |
let head_id = repo.head_id().expect("Current repo has a HEAD"); | |
head_id | |
.object() | |
.unwrap() | |
.try_into_commit() | |
.unwrap() | |
.parent_ids() | |
.next() | |
.unwrap() | |
}; | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
#[test] | |
fn branch_previous() { | |
let repo = test_repo(); | |
let input = "master^"; | |
let expected = { | |
let id = repo | |
.find_reference("master") | |
.unwrap() | |
.into_fully_peeled_id() | |
.unwrap(); | |
id.object() | |
.unwrap() | |
.try_into_commit() | |
.unwrap() | |
.parent_ids() | |
.next() | |
.unwrap() | |
}; | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
} | |
#[cfg(test2)] | |
mod reflog { | |
use super::{parse_revision, test_repo}; | |
#[test] | |
fn at_with_date() { | |
let repo = test_repo(); | |
let input = "@{2021-01-01}"; | |
let msg = format!("Parsed {}", input); | |
let _actual = parse_revision(&repo, input).expect(&msg); | |
} | |
#[test] | |
fn at_with_number() { | |
let repo = test_repo(); | |
let input = "@{1}"; | |
let head_id = repo.head_id().expect("Current repo has a HEAD"); | |
let expected = head_id.ancestors().all().unwrap().next().unwrap().unwrap(); | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
#[test] | |
fn refname_with_date() { | |
let repo = test_repo(); | |
let input = "master@{2021-01-01}"; | |
let msg = format!("Parsed {}", input); | |
let _actual = parse_revision(&repo, input).expect(&msg); | |
} | |
#[test] | |
fn refname_with_number() { | |
let repo = test_repo(); | |
let input = "master@{1}"; | |
let master_id = repo.rev_parse("master").expect("Branch master exists"); | |
let expected = master_id | |
.ancestors() | |
.all() | |
.unwrap() | |
.next() | |
.unwrap() | |
.unwrap(); | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
} | |
#[cfg(test2)] | |
mod remote { | |
use crate::gitrevisions::{parse_revision, test_repo}; | |
#[test] | |
fn master_upstream() { | |
let repo = test_repo(); | |
let input = "master@{upstream}"; | |
let expected = repo | |
.rev_parse("origin/master") | |
.expect("Branch origin/master exists"); | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
#[test] | |
fn master_u() { | |
let repo = test_repo(); | |
let input = "master@{u}"; | |
let expected = repo | |
.rev_parse("origin/master") | |
.expect("Branch origin/master exists"); | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
#[test] | |
fn master_push() { | |
let repo = test_repo(); | |
let input = "master@{push}"; | |
let expected = repo | |
.rev_parse("origin/master") | |
.expect("Branch origin/master exists"); | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
#[test] | |
fn master_p() { | |
let repo = test_repo(); | |
let input = "master@{p}"; | |
let expected = repo | |
.rev_parse("origin/master") | |
.expect("Branch origin/master exists"); | |
let msg = format!("Parsed {}", input); | |
let actual = parse_revision(&repo, input).expect(&msg); | |
assert_eq!(actual, expected); | |
} | |
} | |
#[cfg(test2)] | |
mod complex { | |
use crate::gitrevisions::{parse_revision, test_repo}; | |
#[test] | |
fn head_prev_and_regex() { | |
let repo = test_repo(); | |
let input = "'HEAD~129^{/Update Cargo.toml}'"; | |
let msg = format!("Parsed {}", input); | |
let _actual = parse_revision(&repo, input).expect(&msg); | |
} | |
#[test] | |
fn at_prev_and_regex() { | |
let repo = test_repo(); | |
let input = "'HEAD~129^{/Update Cargo.toml}'"; | |
let msg = format!("Parsed {}", input); | |
let _actual = parse_revision(&repo, input).expect(&msg); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment