Skip to content

Instantly share code, notes, and snippets.

@kalkin
Created May 12, 2022 13:45
Show Gist options
  • Save kalkin/507cb07aa37b4c9c6fd2ed04706336c0 to your computer and use it in GitHub Desktop.
Save kalkin/507cb07aa37b4c9c6fd2ed04706336c0 to your computer and use it in GitHub Desktop.
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