My personal project and infrastructure archive
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.
 
 
 
 
 
 
nomicon/apps/servers/octopus/supergit/src/branch.rs

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()
}
}