From ff37fae10caf5c6546aa935d6f974f11926e1254 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 2 Aug 2022 21:24:50 +0800 Subject: [PATCH] implement nth prior checkout (#427) 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. --- git-repository/src/head.rs | 2 +- .../src/revision/spec/parse/delegate.rs | 56 ++++++++++++++++++- .../src/revision/spec/parse/types.rs | 8 ++- .../tests/revision/spec/from_bytes/reflog.rs | 14 +++-- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/git-repository/src/head.rs b/git-repository/src/head.rs index ac3c897341..2a7c1cc991 100644 --- a/git-repository/src/head.rs +++ b/git-repository/src/head.rs @@ -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"), diff --git a/git-repository/src/revision/spec/parse/delegate.rs b/git-repository/src/revision/spec/parse/delegate.rs index 574f92e23a..5c848908e5 100644 --- a/git-repository/src/revision/spec/parse/delegate.rs +++ b/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; @@ -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 + '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<()> { diff --git a/git-repository/src/revision/spec/parse/types.rs b/git-repository/src/revision/spec/parse/types.rs index 60f3e2ba6c..eeae0d77c7 100644 --- a/git-repository/src/revision/spec/parse/types.rs +++ b/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)] @@ -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" )] @@ -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), diff --git a/git-repository/tests/revision/spec/from_bytes/reflog.rs b/git-repository/tests/revision/spec/from_bytes/reflog.rs index 7fc7676480..6c65e95742 100644 --- a/git-repository/tests/revision/spec/from_bytes/reflog.rs +++ b/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" ); }