You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
271 lines
8.3 KiB
271 lines
8.3 KiB
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<Repository>,
|
|
pub name: Option<String>,
|
|
pub head: HashId,
|
|
}
|
|
|
|
impl Branch {
|
|
/// Create a new branch handle
|
|
pub(crate) fn new(repo: &Arc<Repository>, name: String, head: HashId) -> Self {
|
|
Self {
|
|
repo: Arc::clone(repo),
|
|
name: Some(name),
|
|
head,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn without_name(repo: &Arc<Repository>, head: HashId) -> Self {
|
|
Self {
|
|
repo: Arc::clone(repo),
|
|
name: None,
|
|
head,
|
|
}
|
|
}
|
|
|
|
/// Get a branch handle starting at a certain commit
|
|
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<Repository>,
|
|
last: HashId,
|
|
cmd: IterCmd,
|
|
limit: SegLimit,
|
|
}
|
|
|
|
impl BranchIter {
|
|
/// Create a new branch segment iterator
|
|
fn new(repo: Arc<Repository>, last: HashId, limit: SegLimit) -> Self {
|
|
Self {
|
|
repo,
|
|
last,
|
|
cmd: IterCmd::Step,
|
|
limit,
|
|
}
|
|
}
|
|
|
|
pub fn current(&self) -> Commit {
|
|
Commit::new(&self.repo, self.last.clone()).unwrap()
|
|
}
|
|
|
|
/// Get a commit object, if it exists
|
|
fn find_commit(&self, id: &HashId) -> Option<Commit> {
|
|
Commit::new(&self.repo, id.clone())
|
|
}
|
|
|
|
/// Utility functiot to set last commit
|
|
fn set_last(&mut self, (bc, cmd): (BranchCommit, IterCmd)) -> BranchCommit {
|
|
self.last = bc.id();
|
|
self.cmd = cmd;
|
|
bc
|
|
}
|
|
|
|
/// Get the parent, set the last, and return BranchCommit (maybe)
|
|
fn get_parent(&self, last: Option<Commit>, cmd: IterCmd) -> Option<(BranchCommit, IterCmd)> {
|
|
if let Some(id) = cmd.take() {
|
|
let commit = Commit::new(&self.repo, id).unwrap();
|
|
|
|
return match commit.parent_count() {
|
|
// Special case: if the previous commit was a merge,
|
|
// but this was the first commit in the history, we
|
|
// need to return it here or else it will be forgotten
|
|
// about!
|
|
0 | 1 => Some((BranchCommit::Commit(commit), IterCmd::Step)),
|
|
2 => {
|
|
let p1 = commit.first_parent().unwrap();
|
|
let p2 = commit.parent(1).unwrap();
|
|
|
|
Some((
|
|
BranchCommit::Merge(
|
|
// Here we return a commit via the merge
|
|
// field, because otherwise it will be
|
|
// dropped! Because we just skipped
|
|
// because of a merge (because merges are
|
|
// normal commit parents).
|
|
Some(commit.clone()),
|
|
Branch::without_name(&self.repo, p2.id),
|
|
),
|
|
IterCmd::Skip(p1.id),
|
|
))
|
|
}
|
|
_ => todo!(),
|
|
};
|
|
}
|
|
|
|
// This code is only entered when we are checking for the parents
|
|
last.and_then(|c| match c.parent_count() {
|
|
// No parent means we've reached the end of the branch
|
|
0 => None,
|
|
// One parent is a normal commit
|
|
1 => {
|
|
let parent = c.first_parent().unwrap();
|
|
Some((BranchCommit::Commit(parent), IterCmd::Step))
|
|
}
|
|
// Two parents is a normal merge commit
|
|
2 => {
|
|
let p1 = c.first_parent().unwrap();
|
|
let p2 = c.parent(1).unwrap();
|
|
|
|
Some((
|
|
// Set the Merge commit field to None because it's
|
|
// used to communicate special states (like a
|
|
// merge after another merge).
|
|
BranchCommit::Merge(None, Branch::without_name(&self.repo, p2.id)),
|
|
IterCmd::Skip(p1.id),
|
|
))
|
|
}
|
|
// More or negative parents means the universe is ending
|
|
_ => panic!("Octopus merges are not implemented yet!"),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Iterator for BranchIter {
|
|
type Item = BranchCommit;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
let cmd = mem::replace(&mut self.cmd, IterCmd::Step);
|
|
let last = self.find_commit(&self.last);
|
|
|
|
match self.limit {
|
|
// Get commits forever
|
|
SegLimit::None => self.get_parent(last, cmd).map(|bc| self.set_last(bc)),
|
|
// Get commits until hitting a certain ID
|
|
SegLimit::Commit(ended, _) if ended => None,
|
|
SegLimit::Commit(_, ref c) => {
|
|
let c = c.clone();
|
|
self.get_parent(last, cmd)
|
|
.map(|(bc, cmd)| {
|
|
// Set iterator to "done" if we have reached the commit
|
|
if bc.id() == c {
|
|
self.limit = SegLimit::Commit(true, c.clone());
|
|
(bc, cmd)
|
|
} else {
|
|
(bc, cmd)
|
|
}
|
|
})
|
|
// Set last in case there's more to iterate
|
|
.map(|bc| self.set_last(bc))
|
|
}
|
|
// Get a certain number of commits
|
|
SegLimit::Length(ref mut curr, ref mut max) => {
|
|
if curr >= max {
|
|
return None;
|
|
}
|
|
|
|
*curr += 1;
|
|
self.get_parent(last, cmd).map(|bc| self.set_last(bc))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Specify how to trace actions on the iterator
|
|
#[derive(Debug)]
|
|
enum IterCmd {
|
|
/// Set the last commit to an ID
|
|
Step,
|
|
/// Specify a parent to step to next
|
|
Skip(HashId),
|
|
}
|
|
|
|
impl IterCmd {
|
|
fn take(self) -> Option<HashId> {
|
|
match self {
|
|
Self::Skip(id) => Some(id),
|
|
Self::Step => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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(Option<Commit>, Branch),
|
|
/// An octopus merge with multiple branches
|
|
Octopus(Commit, Vec<Branch>),
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|