//! Type system for working with git branches use crate::{Commit, HashId}; use atomptr::AtomPtr; use git2::Repository; use std::{ mem, sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, }; /// Abstraction for a branch history slice /// /// Git implements an acyclical graph, where branches can be split, /// and re-merge later. Traversal always happens from some point /// onwards, backwards through the history. Because git repositories /// can get quite large and this is a recursive process, it's very /// quickly possible to overflow your program stack. To avoid this, /// `supergit` uses an iterator design to enumerate commits. /// /// Use the API on this type to specify your starting point. By /// default, it will be the head of the branch you are looking at. /// Note: not all branches have names! /// /// After creating a `BranchIter` you can then call `next()` on it, /// yielding `BranchCommit` objects. These can either be single /// commits, or various types of merge commits. Each merge commit /// yields some set of `Branch` handles, that you can either traverse /// by building another `BranchIter`. /// /// A branch iterator is therefore always first-parent, meaning that /// merged branches can simply be ignored by only ever inspecting the /// current `Commit` contained by a `BranchCommit`. #[derive(Clone)] pub struct Branch { repo: Arc, name: Option, 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()), } } /// Create a branch iterator that stops when reaching a commit pub fn get_to(&self, commit: HashId) -> BranchIter { BranchIter::new( Arc::clone(&self.repo), self.head.clone(), SegLimit::Commit(false, commit), ) } /// Create a step-limited branch iterator /// /// This type of iterator is especially useful when combined with /// `skip()`, to create a paginated view onto commits. pub fn get(&self, num: usize) -> BranchIter { BranchIter::new( Arc::clone(&self.repo), self.head.clone(), SegLimit::Length(0, num), ) } /// Create an endless branch iterator /// /// While the creation of the iterator is instantanious, actually /// enumerating all commits in a repository can be quite /// computationally intensive and is almost never what you /// actually want. pub fn get_all(&self) -> BranchIter { BranchIter::new(Arc::clone(&self.repo), self.head.clone(), SegLimit::None) } /// Get the current HEAD commit pub fn head(&self) -> Commit { Commit::new(&self.repo, self.head.clone()).unwrap() } /// Get the branch name, if it exists pub fn name(&self) -> Option { self.name.clone() } } /// A branch slice iterator, created via `Branch` handle /// /// This iterator yields `BranchCommit` objects, that can either be /// simple commits, or various types of merge commits with new Branch /// handles. This means that without explicitly branching, this /// iterator is first-parent. pub struct BranchIter { mode: AtomPtr, splits: IterData, repo: Arc, curr: Option, limit: SegLimit, } impl BranchIter { /// Create a new branch segment iterator fn new(repo: Arc, last: HashId, limit: SegLimit) -> Self { Self { mode: AtomPtr::new(IterMode::FirstParent), splits: IterData::new(), 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(), ), } } /// Determine which commit should be looked at next /// /// FirstParent and DepthFirst iterators will take the next /// first_parent if available. DepthFirst iterators will fall /// back to the last split point, if no `first_parent()` exists. /// BreathFirst iterators will always prefer a new branch over /// first_parent, and jump back to the last split if there are no /// parents. fn determine_next(&mut self, current: Commit) -> Commit { let mode = &**self.mode.get_ref(); self.curr = match mode { IterMode::FirstParent => match current.first_parent() { Some(p1) => Some(p1.id), None => None, }, // DepthFirst iterates normally until we hit the end of // the branch, then we "jump back" to an earlier commit. IterMode::DepthFirst => match current.first_parent() { Some(p1) => Some(p1.id), None => { // Get the last split point. If there are no // more, terminate the iterator. If there are, // increment brnum and keep going. If brnum is // higher than the parent count, call this // function again to get the next split point (and // reset brnum) self.splits.next().map(|id| { let brnum = self.splits.incr_brnum(); // ALSO: when brnum is LOWER than parent point, // re-insert split, to allow the commit to be // jumped to again! if brnum < current.parent_count() { self.splits.re_insert(id.clone()); } let com = self.find_commit(&id).unwrap(); if brnum > current.parent_count() { self.splits.reset_brnum(); self.determine_next(com).id } else { id } }) } }, // // When there is only one parent, chose that parent. When // // there is none, jump back to the last split point, // // according to brnum. If brnum is then greater than the // // number of branches, reset brnum and re-call this // // function. // IterMode::BreadthFirst if current.parent_count() <= 1 => match current.first_parent() { // Some(p1) => Some(p1.id), // None => self.splits.next().map(|id| { // let brnum = self.splits.incr_brnum(); // }), // }, // // When there are is more than 1 parent, set this commit // // as a split point, and chose the last parent commit as // // the next commit to walk. This shifts the active branch // // over. Important: we set brnum to the _highest_ branch, // // and iterate inwards. // IterMode::BreadthFirst => { // }, _ => todo!(), }; 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 { let mode = &**self.mode.get_ref(); let id = match mode { // When iterating first-parent OR when going depth first, // while the current branch still has children, take the // next child of the current branch. IterMode::FirstParent => mem::replace(&mut self.curr, None), IterMode::DepthFirst | IterMode::BreadthFirst if self.curr.is_some() => { mem::replace(&mut self.curr, None) } // When going both BreadthFirst or DepthFirst, reaching // the end of the current branch means getting the last // split point. The difference between these two // strategies is in how the split points and "current // point" are stored. _ if self.curr.is_none() => self.splits.next(), _ => unreachable!(), // can't be reached }; // The chain of events id.and_then(|id| self.find_commit(&id)) .map(|c| self.determine_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)) } } /// Specify the mode of a branch iterator /// /// The default value is `FirstParent`, meaning that merged branches /// will create a new `Branch` handle that the user must iterate /// manually. /// /// This is a reasonable default, but means that history searches for /// files become more tedious. To improve this use-case, iterators /// can internally be set to change their iteration behaviour, meaning /// that returned commits are always `BranchCommit::Commit`, and can pub enum IterMode { /// Default value, iterating only first-parent commits FirstParent, /// Iterate a branch to completion, before picking the next DepthFirst, /// Iterate branches as they come up BreadthFirst, } /// Limiter 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() } /// Get the underlying commit, regardless of type pub fn commit(&self) -> &Commit { use BranchCommit::*; match self { Commit(ref c) => c, Merge(ref c, _) => c, Octopus(ref c, _) => c, } } } /// Additional iterator data /// /// This structure tracks split points on the iterator. When /// traversing a branch, the iterator can reach branch points. While /// in `IterMode::FirstParent`, these are irrelevant. However: when /// iterating breadth or depth first these need to be tracked. /// /// ## Depth first /// /// - When a branch is encountered, append it to this data set. /// - When the end of the current branch is reached, get the next /// split point to resume from /// /// ## Breadth first /// /// - When a branch is encountered, add the previous commit to this set /// - When reaching the end of a branch, get the next split point to /// resume from /// /// --- /// /// In essense, the usage of this structure for BreadthFirst and /// DepthFirst are inverted! struct IterData { /// Split points on the iterator splits: AtomPtr>, /// Additional data for octopus merges brnum: AtomicUsize, } impl IterData { fn new() -> Self { Self { splits: AtomPtr::new(vec![]), brnum: AtomicUsize::new(0), } } /// Check if the split set is empty fn empty(&self) -> bool { self.splits.get_ref().len() == 0 } fn set_brnum(&self, num: usize) { self.brnum.swap(num, Ordering::Relaxed); } fn incr_brnum(&self) -> usize { self.brnum.fetch_add(1, Ordering::Relaxed) + 1 } fn decr_brnum(&self) -> usize { self.brnum.fetch_sub(1, Ordering::Relaxed) - 1 } fn reset_brnum(&self) { self.set_brnum(0); } fn append(&self, id: HashId) { let mut vec = (**self.splits.get_ref()).clone(); let mut new = vec![id]; new.append(&mut vec); self.splits.swap(new); } /// Insert a hashID to the front of the splits list fn re_insert(&self, id: HashId) { let mut vec = (**self.splits.get_ref()).clone(); vec.insert(0, id); self.splits.swap(vec); } fn next(&self) -> Option { let mut vec = (**self.splits.get_ref()).clone(); let next = if vec.len() < 0 { Some(vec.remove(0)) } else { None }; self.splits.swap(vec); next } }