Skip to content

Commit

Permalink
implement nth prior checkout (#427)
Browse files Browse the repository at this point in the history
What's nice is that it keeps track of the reference as well, which is
a benefit. It will also do its best to not fail if the branch doesn't
exist anymore but instead resorts to the id available in the reflog.
  • Loading branch information
Byron committed Aug 2, 2022
1 parent 4fd2314 commit ff37fae
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 9 deletions.
2 changes: 1 addition & 1 deletion git-repository/src/head.rs
Expand Up @@ -89,7 +89,7 @@ pub mod log {

impl<'repo> Head<'repo> {
/// Return a platform for obtaining iterators on the reference log associated with the `HEAD` reference.
pub fn log_iter(&self) -> git_ref::file::log::iter::Platform<'static, '_> {
pub fn log_iter(&self) -> git_ref::file::log::iter::Platform<'static, 'repo> {
git_ref::file::log::iter::Platform {
store: &self.repo.refs,
name: "HEAD".try_into().expect("HEAD is always valid"),
Expand Down
56 changes: 53 additions & 3 deletions git-repository/src/revision/spec/parse/delegate.rs
@@ -1,5 +1,5 @@
use super::{Delegate, Error, ObjectKindHint, RefsHint};
use crate::bstr::{BStr, ByteSlice};
use crate::bstr::{BStr, BString, ByteSlice};
use crate::ext::{ObjectIdExt, ReferenceExt};
use crate::{object, Repository};
use git_hash::ObjectId;
Expand Down Expand Up @@ -276,9 +276,59 @@ impl<'repo> delegate::Revision for Delegate<'repo> {
todo!()
}

fn nth_checked_out_branch(&mut self, _branch_no: usize) -> Option<()> {
fn nth_checked_out_branch(&mut self, branch_no: usize) -> Option<()> {
self.unset_disambiguate_call();
todo!()
fn prior_checkouts_iter<'a>(
platform: &'a mut git_ref::file::log::iter::Platform<'static, '_>,
) -> Result<impl Iterator<Item = (BString, ObjectId)> + 'a, Error> {
match platform.rev().ok().flatten() {
Some(log) => Ok(log.filter_map(Result::ok).filter_map(|line| {
line.message
.strip_prefix(b"checkout: moving from ")
.and_then(|from_to| from_to.find(" to ").map(|pos| &from_to[..pos]))
.map(|from_branch| (from_branch.into(), line.previous_oid))
})),
None => Err(Error::MissingRefLog {
reference: "HEAD".into(),
action: "search prior checked out branch",
}),
}
}

let head = match self.repo.head() {
Ok(head) => head,
Err(err) => {
self.err.push(err.into());
return None;
}
};
match prior_checkouts_iter(&mut head.log_iter()).map(|mut it| it.nth(branch_no.saturating_sub(1))) {
Ok(Some((ref_name, id))) => {
let id = match self.repo.find_reference(ref_name.as_bstr()) {
Ok(mut r) => {
let id = r.peel_to_id_in_place().map(|id| id.detach()).unwrap_or(id);
self.refs[self.idx] = Some(r.detach());
id
}
Err(_) => id,
};
self.objs[self.idx].get_or_insert_with(HashSet::default).insert(id);
Some(())
}
Ok(None) => {
self.err.push(Error::PriorCheckoutOutOfRange {
desired: branch_no,
available: prior_checkouts_iter(&mut head.log_iter())
.map(|it| it.count())
.unwrap_or(0),
});
None
}
Err(err) => {
self.err.push(err);
None
}
}
}

fn sibling_branch(&mut self, _kind: SiblingBranch) -> Option<()> {
Expand Down
8 changes: 7 additions & 1 deletion git-repository/src/revision/spec/parse/types.rs
@@ -1,5 +1,5 @@
use crate::bstr::BString;
use crate::object;
use crate::{object, reference};

/// A hint to know what to do if refs and object names are equal.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -61,6 +61,10 @@ pub struct Options {
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Reference {reference:?} does not have a reference log, cannot {action}")]
MissingRefLog { reference: BString, action: &'static str },
#[error("HEAD has {available} prior checkouts and checkout number {desired} is out of range")]
PriorCheckoutOutOfRange { desired: usize, available: usize },
#[error(
"Commit {oid} has {available} ancestors along the first parent and ancestor number {desired} is out of range"
)]
Expand All @@ -83,6 +87,8 @@ pub enum Error {
exists: bool,
},
#[error(transparent)]
FindHead(#[from] reference::find::existing::Error),
#[error(transparent)]
Index(#[from] crate::worktree::open_index::Error),
#[error(transparent)]
RevWalkIterInit(#[from] crate::reference::iter::init::Error),
Expand Down
14 changes: 10 additions & 4 deletions git-repository/tests/revision/spec/from_bytes/reflog.rs
@@ -1,16 +1,22 @@
use crate::revision::spec::from_bytes::{parse_spec, repo};

#[test]
#[ignore]
fn nth_prior_checkout() {
let repo = repo("complex_graph").unwrap();

for spec in ["@{-1}", "@{-2}", "@{-3}", "@{-4}", "@{-5}"] {
assert!(parse_spec(spec, &repo).is_ok(), "spec {} should be valid", spec);
for (spec, prior_branch) in [
("@{-1}", "refs/heads/i"),
("@{-2}", "refs/heads/main"),
("@{-3}", "refs/heads/e"),
("@{-4}", "refs/heads/j"),
("@{-5}", "refs/heads/h"),
] {
let parsed = parse_spec(spec, &repo).unwrap_or_else(|_| panic!("{} to be parsed successfully", spec));
assert_eq!(parsed.first_reference().expect("present").name.as_bstr(), prior_branch);
}

assert_eq!(
parse_spec("@{-6}", &repo).unwrap_err().to_string(),
"HEAD has 5 prior checkouts and checkout 6 is out of range"
"HEAD has 5 prior checkouts and checkout number 6 is out of range"
);
}

0 comments on commit ff37fae

Please sign in to comment.