use crate::ObjectId;
use crate::cherry_pick::advance_head;
use crate::dag;
use crate::error::VcsError;
use crate::merge;
use crate::object::{CommitObject, Object};
use crate::store::{self, Store};
pub fn rebase(store: &mut dyn Store, onto: ObjectId, author: &str) -> Result<ObjectId, VcsError> {
let head_id = store::resolve_head(store)?.ok_or_else(|| VcsError::RefNotFound {
name: "HEAD".to_owned(),
})?;
let base_id = dag::merge_base(store, head_id, onto)?.ok_or(VcsError::NoCommonAncestor)?;
let path = dag::find_path(store, base_id, head_id)?;
let commits_to_replay: Vec<ObjectId> = path.into_iter().skip(1).collect();
if commits_to_replay.is_empty() {
return Ok(head_id);
}
let old_head = head_id;
let mut current_tip = onto;
match store.get_head()? {
crate::HeadState::Branch(name) => {
let ref_name = format!("refs/heads/{name}");
store.set_ref(&ref_name, onto)?;
}
crate::HeadState::Detached(_) => {
store.set_head(crate::HeadState::Detached(onto))?;
}
}
for commit_id in &commits_to_replay {
current_tip = replay_one(store, *commit_id, current_tip, author)?;
}
advance_head(
store,
old_head,
current_tip,
author,
&format!("rebase onto {}", onto.short()),
)?;
Ok(current_tip)
}
fn replay_one(
store: &mut dyn Store,
commit_id: ObjectId,
current_tip: ObjectId,
author: &str,
) -> Result<ObjectId, VcsError> {
let commit = load_commit(store, commit_id)?;
let parent_id = commit.parents.first().ok_or(VcsError::NoPath)?;
let parent_commit = load_commit(store, *parent_id)?;
let base_schema = load_schema(store, parent_commit.schema_id)?;
let theirs_schema = load_schema(store, commit.schema_id)?;
let tip_commit = load_commit(store, current_tip)?;
let ours_schema = load_schema(store, tip_commit.schema_id)?;
let result = merge::three_way_merge(&base_schema, &ours_schema, &theirs_schema);
if !result.conflicts.is_empty() {
return Err(VcsError::MergeConflicts {
count: result.conflicts.len(),
});
}
let mig_src = store.put(&Object::FlatSchema(Box::new(ours_schema)))?;
let mig_tgt = store.put(&Object::FlatSchema(Box::new(result.merged_schema.clone())))?;
let merged_schema_id = crate::tree::store_schema_as_tree(store, result.merged_schema)?;
let migration_id = store.put(&Object::Migration {
src: mig_src,
tgt: mig_tgt,
mapping: result.migration_from_ours,
})?;
let new_commit = CommitObject::builder(
merged_schema_id,
commit.protocol.clone(),
author,
commit.message.clone(),
)
.parents(vec![current_tip])
.migration_id(migration_id)
.build();
let new_commit_id = store.put(&Object::Commit(new_commit))?;
match store.get_head()? {
crate::HeadState::Branch(name) => {
let ref_name = format!("refs/heads/{name}");
store.set_ref(&ref_name, new_commit_id)?;
}
crate::HeadState::Detached(_) => {
store.set_head(crate::HeadState::Detached(new_commit_id))?;
}
}
Ok(new_commit_id)
}
fn load_commit(store: &dyn Store, id: ObjectId) -> Result<CommitObject, VcsError> {
match store.get(&id)? {
Object::Commit(c) => Ok(c),
other => Err(VcsError::WrongObjectType {
expected: "commit",
found: other.type_name(),
}),
}
}
fn load_schema(
store: &dyn Store,
schema_id: ObjectId,
) -> Result<panproto_schema::Schema, VcsError> {
let proto = crate::tree::project_coproduct_protocol();
crate::tree::assemble_schema_dyn(store, &schema_id, &proto)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemStore;
use crate::error::VcsError;
use panproto_gat::Name;
use panproto_schema::{Schema, Vertex};
use std::collections::HashMap;
fn make_schema(vertices: &[(&str, &str)]) -> Schema {
let mut vert_map = HashMap::new();
for (id, kind) in vertices {
vert_map.insert(
Name::from(*id),
Vertex {
id: Name::from(*id),
kind: Name::from(*kind),
nsid: None,
},
);
}
Schema {
protocol: "test".into(),
vertices: vert_map,
edges: HashMap::new(),
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
}
}
#[test]
fn rebase_linear_onto_diverged() -> Result<(), VcsError> {
let mut store = MemStore::new();
let s0 = make_schema(&[("a", "object")]);
let s0_id = crate::tree::store_schema_as_tree(&mut store, s0)?;
let c0 = CommitObject::builder(s0_id, "test", "alice", "initial")
.timestamp(100)
.build();
let c0_id = store.put(&Object::Commit(c0))?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
let s1_id = crate::tree::store_schema_as_tree(&mut store, s1)?;
let c1 = CommitObject::builder(s1_id, "test", "alice", "add b")
.parents(vec![c0_id])
.timestamp(200)
.build();
let c1_id = store.put(&Object::Commit(c1))?;
let s2 = make_schema(&[("a", "object"), ("c", "integer")]);
let s2_id = crate::tree::store_schema_as_tree(&mut store, s2)?;
let c2 = CommitObject::builder(s2_id, "test", "bob", "add c")
.parents(vec![c0_id])
.timestamp(300)
.build();
let c2_id = store.put(&Object::Commit(c2))?;
store.set_ref("refs/heads/main", c2_id)?;
let new_tip = rebase(&mut store, c1_id, "bob")?;
let new_commit = match store.get(&new_tip)? {
Object::Commit(c) => c,
other => {
return Err(VcsError::WrongObjectType {
expected: "commit",
found: other.type_name(),
});
}
};
let new_schema = crate::tree::resolve_commit_schema(&store, &new_commit)?;
assert!(new_schema.vertices.contains_key("a"));
assert!(new_schema.vertices.contains_key("b"));
assert!(new_schema.vertices.contains_key("c"));
assert_eq!(new_commit.parents[0], c1_id);
Ok(())
}
}