use crate::{Commit, HashId}; use git2::Repository; use std::{mem, sync::Arc}; /// Abstraction for a branch history slice /// /// #[derive(Clone)] pub struct Branch { repo: Arc, pub name: Option, pub head: HashId, } impl Branch { /// Create a new branch handle pub(crate) fn new(repo: &Arc, name: String, head: HashId) -> Self { Self { repo: Arc::clone(repo), name: Some(name), head, } } pub(crate) fn without_name(repo: &Arc, head: HashId) -> Self { Self { repo: Arc::clone(repo), name: None, head, } } /// Get a branch handle starting at a certain commit // TODO: do we want to check if this is actually a child? pub fn skip_to(&self, from: HashId) -> Self { match self.name { Some(ref name) => Self::new(&self.repo, name.clone(), from), None => Self::without_name(&self.repo, from), } } /// Create a branch handle that skips a certain number of commits /// /// This walker always picks the first parent. pub fn skip(&self, num: usize) -> Self { let mut head = self.repo.find_commit(self.head.clone().into()).unwrap(); for _ in 0..num { if let Ok(p) = head.parent(0) { head = p; } } match self.name { Some(ref name) => Self::new(&self.repo, name.clone(), head.id().into()), None => Self::without_name(&self.repo, head.id().into()), } } pub fn get_to(&self, commit: HashId) -> BranchIter { BranchIter::new( Arc::clone(&self.repo), self.head.clone(), SegLimit::Commit(false, commit), ) } /// Get the primary branch history as far back as it goes pub fn get_all(&self) -> BranchIter { BranchIter::new(Arc::clone(&self.repo), self.head.clone(), SegLimit::None) } /// Get a branch segment of a certain length pub fn get(&self, num: usize) -> BranchIter { BranchIter::new( Arc::clone(&self.repo), self.head.clone(), SegLimit::Length(0, num), ) } } /// A branch segment iterator /// /// Each iterator is first-parent, but will notify you about a split /// parent by setting pub struct BranchIter { repo: Arc, curr: Option, limit: SegLimit, } impl BranchIter { /// Create a new branch segment iterator fn new(repo: Arc, last: HashId, limit: SegLimit) -> Self { Self { repo, curr: Some(last), limit, } } pub fn current(&self) -> Commit { Commit::new(&self.repo, self.curr.as_ref().unwrap().clone()).unwrap() } /// Get a commit object, if it exists fn find_commit(&self, id: &HashId) -> Option { Commit::new(&self.repo, id.clone()) } /// For a current commit, get it's parents if they exists fn parents(&self, curr: &Commit) -> (Option, Option) { (curr.first_parent(), curr.parent(1)) } /// Take an optional commit and turn it into a branch commit fn make_branch_commit(&self, curr: Commit) -> BranchCommit { match curr.parent_count() { 0 | 1 => BranchCommit::Commit(curr), 2 => { let p2 = self.parents(&curr).1.unwrap(); BranchCommit::Merge(curr, Branch::without_name(&self.repo, p2.id)) } _ => BranchCommit::Octopus( curr.clone(), curr.parents() .into_iter() .map(|c| Branch::without_name(&self.repo, c.id)) .collect(), ), } } /// Get the current commit /// /// This function looks either at the "curr" field, or takes the /// ID from `cmd`, if it is set to `IterCmd::Jump(...)`, which /// indicates that the previous commit was a merge, and we need to escape fn set_next(&mut self, current: Commit) -> Commit { self.curr = match current.first_parent() { Some(p1) => Some(p1.id), None => None, }; current } } impl Iterator for BranchIter { type Item = BranchCommit; fn next(&mut self) -> Option { mem::replace(&mut self.curr, None) .and_then(|id| self.find_commit(&id)) .map(|c| self.set_next(c)) .and_then(|c| match self.limit { SegLimit::None => Some(c), SegLimit::Commit(ended, _) if ended => None, SegLimit::Commit(ref mut b, ref target) => { if &c.id == target { *b = true; } Some(c) } SegLimit::Length(ref mut curr, ref max) if *curr < *max => { *curr += 1; Some(c) } SegLimit::Length(ref curr, ref mut max) if curr >= max => None, SegLimit::Length(_, _) => unreachable!(), // oh rustc :) }) .map(|c| self.make_branch_commit(c)) } } /// the limit applied to a branch segment pub enum SegLimit { /// No limit, enumerating all children None, /// Run until a certain commit is found Commit(bool, HashId), /// Run to collect a certain number of commits Length(usize, usize), } /// A commit represented as a relationship to a branch /// /// Most commits will be simple, meaning they are in sequence on the /// branch. Two types of merge commits exist: normal, and octopus. /// All branches leading into this branch are a reverse tree pub enum BranchCommit { /// A single commit Commit(Commit), /// A merge commit from one other branch Merge(Commit, Branch), /// An octopus merge with multiple branches Octopus(Commit, Vec), } impl BranchCommit { pub fn id(&self) -> HashId { use BranchCommit::*; match self { Commit(ref c) => &c.id, Merge(_, ref b) => &b.head, Octopus(ref c, _) => &c.id, } .clone() } }