cob: Redesign merge state
Move `merges` from `Revision` to `Patch`, and key them by actor. This ensures that only one merge is possible per actor. We also make sure that only one revision can be merged at a time, and signal a conflict if more than one revision is considered merged. Finally, we make sure that merges have the same target-branch commit to be considered equal.
This commit is contained in:
parent
8d8751655f
commit
2d55ffba81
|
|
@ -101,6 +101,27 @@ pub enum OperationName {
|
||||||
Edit,
|
Edit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Filter(fn(&patch::State) -> bool);
|
||||||
|
|
||||||
|
impl Filter {
|
||||||
|
/// Match everything.
|
||||||
|
fn all() -> Self {
|
||||||
|
Self(|_| true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Filter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(|state| matches!(state, patch::State::Open { .. }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Filter {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Filter(..)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
Open {
|
Open {
|
||||||
|
|
@ -131,7 +152,7 @@ pub enum Operation {
|
||||||
patch_id: Rev,
|
patch_id: Rev,
|
||||||
},
|
},
|
||||||
List {
|
List {
|
||||||
filter: Option<patch::State>,
|
filter: Filter,
|
||||||
},
|
},
|
||||||
Edit {
|
Edit {
|
||||||
patch_id: Rev,
|
patch_id: Rev,
|
||||||
|
|
@ -160,7 +181,7 @@ impl Args for Options {
|
||||||
let mut patch_id = None;
|
let mut patch_id = None;
|
||||||
let mut message = Message::default();
|
let mut message = Message::default();
|
||||||
let mut push = true;
|
let mut push = true;
|
||||||
let mut filter = Some(patch::State::Open);
|
let mut filter = Filter::default();
|
||||||
let mut diff = false;
|
let mut diff = false;
|
||||||
let mut draft = false;
|
let mut draft = false;
|
||||||
let mut undo = false;
|
let mut undo = false;
|
||||||
|
|
@ -220,19 +241,19 @@ impl Args for Options {
|
||||||
|
|
||||||
// List options.
|
// List options.
|
||||||
Long("all") => {
|
Long("all") => {
|
||||||
filter = None;
|
filter = Filter::all();
|
||||||
}
|
}
|
||||||
Long("draft") => {
|
Long("draft") => {
|
||||||
filter = Some(patch::State::Draft);
|
filter = Filter(|s| s == &patch::State::Draft);
|
||||||
}
|
}
|
||||||
Long("archived") => {
|
Long("archived") => {
|
||||||
filter = Some(patch::State::Archived);
|
filter = Filter(|s| s == &patch::State::Archived);
|
||||||
}
|
}
|
||||||
Long("merged") => {
|
Long("merged") => {
|
||||||
filter = Some(patch::State::Merged);
|
filter = Filter(|s| matches!(s, patch::State::Merged { .. }));
|
||||||
}
|
}
|
||||||
Long("open") => {
|
Long("open") => {
|
||||||
filter = Some(patch::State::Open);
|
filter = Filter(|s| matches!(s, patch::State::Open { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common.
|
// Common.
|
||||||
|
|
@ -352,8 +373,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
options,
|
options,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Operation::List { filter } => {
|
Operation::List { filter: Filter(f) } => {
|
||||||
list::run(&repository, &profile, filter)?;
|
list::run(&repository, &profile, f)?;
|
||||||
}
|
}
|
||||||
Operation::Show { patch_id, diff } => {
|
Operation::Show { patch_id, diff } => {
|
||||||
let patch_id = patch_id.resolve(&repository.backend)?;
|
let patch_id = patch_id.resolve(&repository.backend)?;
|
||||||
|
|
|
||||||
|
|
@ -178,9 +178,7 @@ pub fn find_unmerged_with_base(
|
||||||
let mut matches = Vec::new();
|
let mut matches = Vec::new();
|
||||||
|
|
||||||
for (id, patch, clock) in proposed {
|
for (id, patch, clock) in proposed {
|
||||||
let (_, rev) = patch.latest().unwrap();
|
if patch.merges().count() != 0 {
|
||||||
|
|
||||||
if !rev.merges().count() == 0 {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if **patch.head() == patch_head {
|
if **patch.head() == patch_head {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use super::common;
|
||||||
pub fn run(
|
pub fn run(
|
||||||
repository: &Repository,
|
repository: &Repository,
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
filter: Option<patch::State>,
|
filter: fn(&patch::State) -> bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let patches = Patches::open(repository)?;
|
let patches = Patches::open(repository)?;
|
||||||
|
|
||||||
|
|
@ -24,11 +24,9 @@ pub fn run(
|
||||||
for patch in patches.all()? {
|
for patch in patches.all()? {
|
||||||
let (id, patch, _) = patch?;
|
let (id, patch, _) = patch?;
|
||||||
|
|
||||||
if let Some(filter) = filter {
|
if !filter(patch.state()) {
|
||||||
if patch.state() != filter {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
all.push((id, patch));
|
all.push((id, patch));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,7 +103,7 @@ pub fn row(
|
||||||
|
|
||||||
Ok([
|
Ok([
|
||||||
match state {
|
match state {
|
||||||
patch::State::Open => term::format::positive("●").into(),
|
patch::State::Open { .. } => term::format::positive("●").into(),
|
||||||
patch::State::Archived { .. } => term::format::yellow("●").into(),
|
patch::State::Archived { .. } => term::format::yellow("●").into(),
|
||||||
patch::State::Draft => term::format::dim("●").into(),
|
patch::State::Draft => term::format::dim("●").into(),
|
||||||
patch::State::Merged { .. } => term::format::primary("✔").into(),
|
patch::State::Merged { .. } => term::format::primary("✔").into(),
|
||||||
|
|
@ -172,8 +170,8 @@ pub fn timeline(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
for merge in revision.merges() {
|
for (nid, merge) in patch.merges().filter(|(_, m)| m.revision == *revision_id) {
|
||||||
let peer = repository.remote(&merge.node)?;
|
let peer = repository.remote(nid)?;
|
||||||
let mut badges = Vec::new();
|
let mut badges = Vec::new();
|
||||||
|
|
||||||
if peer.id == *whoami {
|
if peer.id == *whoami {
|
||||||
|
|
|
||||||
|
|
@ -105,10 +105,10 @@ pub fn run(
|
||||||
attrs.push([
|
attrs.push([
|
||||||
term::format::tertiary("Status".to_owned()).into(),
|
term::format::tertiary("Status".to_owned()).into(),
|
||||||
match state {
|
match state {
|
||||||
patch::State::Open => term::format::positive(state.to_string()),
|
patch::State::Open { .. } => term::format::positive(state.to_string()),
|
||||||
patch::State::Draft => term::format::dim(state.to_string()),
|
patch::State::Draft => term::format::dim(state.to_string()),
|
||||||
patch::State::Archived => term::format::yellow(state.to_string()),
|
patch::State::Archived => term::format::yellow(state.to_string()),
|
||||||
patch::State::Merged => term::format::primary(state.to_string()),
|
patch::State::Merged { .. } => term::format::primary(state.to_string()),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,10 @@ pub enum PatchState {
|
||||||
impl PatchState {
|
impl PatchState {
|
||||||
pub fn matches(&self, patch: &patch::State) -> bool {
|
pub fn matches(&self, patch: &patch::State) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Open => matches!(patch, patch::State::Open),
|
Self::Open => matches!(patch, patch::State::Open { .. }),
|
||||||
Self::Draft => matches!(patch, patch::State::Draft),
|
Self::Draft => matches!(patch, patch::State::Draft),
|
||||||
Self::Archived => matches!(patch, patch::State::Archived),
|
Self::Archived => matches!(patch, patch::State::Archived),
|
||||||
Self::Merged => matches!(patch, patch::State::Merged),
|
Self::Merged => matches!(patch, patch::State::Merged { .. }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,14 @@ pub(crate) fn patch(
|
||||||
"state": patch.state(),
|
"state": patch.state(),
|
||||||
"target": patch.target(),
|
"target": patch.target(),
|
||||||
"tags": patch.tags().collect::<Vec<_>>(),
|
"tags": patch.tags().collect::<Vec<_>>(),
|
||||||
|
"merges": patch.merges().map(|(a, m)| {
|
||||||
|
json!({
|
||||||
|
"author": a,
|
||||||
|
"revision": m.revision,
|
||||||
|
"commit": m.commit,
|
||||||
|
"timestamp": m.timestamp
|
||||||
|
})
|
||||||
|
}).collect::<Vec<_>>(),
|
||||||
"revisions": patch.revisions().map(|(id, rev)| {
|
"revisions": patch.revisions().map(|(id, rev)| {
|
||||||
json!({
|
json!({
|
||||||
"id": id,
|
"id": id,
|
||||||
|
|
@ -130,7 +138,6 @@ pub(crate) fn patch(
|
||||||
"base": rev.base(),
|
"base": rev.base(),
|
||||||
"oid": rev.head(),
|
"oid": rev.head(),
|
||||||
"refs": get_refs(repo, patch.author().id(), &rev.head()).unwrap_or(vec![]),
|
"refs": get_refs(repo, patch.author().id(), &rev.head()).unwrap_or(vec![]),
|
||||||
"merges": rev.merges().collect::<Vec<_>>(),
|
|
||||||
"discussions": rev.discussion().comments()
|
"discussions": rev.discussion().comments()
|
||||||
.map(|(id, comment)| Comment::new(id, comment, rev.discussion(), aliases))
|
.map(|(id, comment)| Comment::new(id, comment, rev.discussion(), aliases))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
|
|
|
||||||
|
|
@ -1745,6 +1745,7 @@ mod routes {
|
||||||
"state": { "status": "open" },
|
"state": { "status": "open" },
|
||||||
"target": "delegates",
|
"target": "delegates",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
"merges": [],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_PATCH_ID,
|
"id": CONTRIBUTOR_PATCH_ID,
|
||||||
|
|
@ -1754,7 +1755,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
|
|
@ -1784,6 +1784,7 @@ mod routes {
|
||||||
"state": { "status": "open" },
|
"state": { "status": "open" },
|
||||||
"target": "delegates",
|
"target": "delegates",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
"merges": [],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_PATCH_ID,
|
"id": CONTRIBUTOR_PATCH_ID,
|
||||||
|
|
@ -1793,7 +1794,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
|
|
@ -1862,6 +1862,7 @@ mod routes {
|
||||||
"state": { "status": "open" },
|
"state": { "status": "open" },
|
||||||
"target": "delegates",
|
"target": "delegates",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
"merges": [],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CREATED_PATCH_ID,
|
"id": CREATED_PATCH_ID,
|
||||||
|
|
@ -1871,7 +1872,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
|
|
@ -1925,6 +1925,7 @@ mod routes {
|
||||||
"bug",
|
"bug",
|
||||||
"design"
|
"design"
|
||||||
],
|
],
|
||||||
|
"merges": [],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_PATCH_ID,
|
"id": CONTRIBUTOR_PATCH_ID,
|
||||||
|
|
@ -1934,7 +1935,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
|
|
@ -1985,6 +1985,7 @@ mod routes {
|
||||||
"state": { "status": "open" },
|
"state": { "status": "open" },
|
||||||
"target": "delegates",
|
"target": "delegates",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
"merges": [],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_PATCH_ID,
|
"id": CONTRIBUTOR_PATCH_ID,
|
||||||
|
|
@ -1994,7 +1995,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
|
|
@ -2007,7 +2007,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
|
|
@ -2058,6 +2057,7 @@ mod routes {
|
||||||
"state": { "status": "open" },
|
"state": { "status": "open" },
|
||||||
"target": "delegates",
|
"target": "delegates",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
"merges": [],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_PATCH_ID,
|
"id": CONTRIBUTOR_PATCH_ID,
|
||||||
|
|
@ -2067,7 +2067,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
|
|
@ -2140,6 +2139,7 @@ mod routes {
|
||||||
"state": { "status": "open" },
|
"state": { "status": "open" },
|
||||||
"target": "delegates",
|
"target": "delegates",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
"merges": [],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_PATCH_ID,
|
"id": CONTRIBUTOR_PATCH_ID,
|
||||||
|
|
@ -2149,7 +2149,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [
|
"discussions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_COMMENT_1,
|
"id": CONTRIBUTOR_COMMENT_1,
|
||||||
|
|
@ -2236,6 +2235,7 @@ mod routes {
|
||||||
"state": { "status": "open" },
|
"state": { "status": "open" },
|
||||||
"target": "delegates",
|
"target": "delegates",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
"merges": [],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_PATCH_ID,
|
"id": CONTRIBUTOR_PATCH_ID,
|
||||||
|
|
@ -2245,7 +2245,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [
|
"reviews": [
|
||||||
|
|
@ -2316,9 +2315,19 @@ mod routes {
|
||||||
},
|
},
|
||||||
"title": "A new `hello world`",
|
"title": "A new `hello world`",
|
||||||
"description": "change `hello world` in README to something else",
|
"description": "change `hello world` in README to something else",
|
||||||
"state": { "status": "merged" },
|
"state": {
|
||||||
|
"status": "merged",
|
||||||
|
"revision": CONTRIBUTOR_PATCH_ID,
|
||||||
|
"commit": PARENT,
|
||||||
|
},
|
||||||
"target": "delegates",
|
"target": "delegates",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
"merges": [{
|
||||||
|
"author": CONTRIBUTOR_NID,
|
||||||
|
"revision": CONTRIBUTOR_PATCH_ID,
|
||||||
|
"commit": PARENT,
|
||||||
|
"timestamp": TIMESTAMP,
|
||||||
|
}],
|
||||||
"revisions": [
|
"revisions": [
|
||||||
{
|
{
|
||||||
"id": CONTRIBUTOR_PATCH_ID,
|
"id": CONTRIBUTOR_PATCH_ID,
|
||||||
|
|
@ -2328,13 +2337,6 @@ mod routes {
|
||||||
"refs": [
|
"refs": [
|
||||||
"refs/heads/master",
|
"refs/heads/master",
|
||||||
],
|
],
|
||||||
"merges": [
|
|
||||||
{
|
|
||||||
"node": CONTRIBUTOR_NID,
|
|
||||||
"commit": PARENT,
|
|
||||||
"timestamp": TIMESTAMP,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"discussions": [],
|
"discussions": [],
|
||||||
"timestamp": TIMESTAMP,
|
"timestamp": TIMESTAMP,
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
@ -11,7 +12,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use radicle_crdt::clock;
|
use radicle_crdt::clock;
|
||||||
use radicle_crdt::{GMap, GSet, LWWReg, LWWSet, Lamport, Max, Redactable, Semilattice};
|
use radicle_crdt::{GMap, GSet, LWWMap, LWWReg, LWWSet, Lamport, Max, Redactable, Semilattice};
|
||||||
|
|
||||||
use crate::cob;
|
use crate::cob;
|
||||||
use crate::cob::common::{Author, Tag, Timestamp};
|
use crate::cob::common::{Author, Tag, Timestamp};
|
||||||
|
|
@ -64,6 +65,9 @@ pub enum ApplyError {
|
||||||
/// Error loading the identity document committed to by an operation.
|
/// Error loading the identity document committed to by an operation.
|
||||||
#[error("identity doc failed to load: {0}")]
|
#[error("identity doc failed to load: {0}")]
|
||||||
Doc(#[from] DocError),
|
Doc(#[from] DocError),
|
||||||
|
/// The merge operation is invalid.
|
||||||
|
#[error("invalid merge operation in {0}")]
|
||||||
|
InvalidMerge(EntryId),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error updating or creating patches.
|
/// Error updating or creating patches.
|
||||||
|
|
@ -157,6 +161,8 @@ pub struct Patch {
|
||||||
target: LWWReg<Max<MergeTarget>>,
|
target: LWWReg<Max<MergeTarget>>,
|
||||||
/// Associated tags.
|
/// Associated tags.
|
||||||
tags: LWWSet<Tag>,
|
tags: LWWSet<Tag>,
|
||||||
|
/// Patch merges.
|
||||||
|
merges: LWWMap<ActorId, Redactable<Merge>>,
|
||||||
/// List of patch revisions. The initial changeset is part of the
|
/// List of patch revisions. The initial changeset is part of the
|
||||||
/// first revision.
|
/// first revision.
|
||||||
revisions: GMap<RevisionId, Redactable<Revision>>,
|
revisions: GMap<RevisionId, Redactable<Revision>>,
|
||||||
|
|
@ -170,6 +176,7 @@ impl Semilattice for Patch {
|
||||||
self.description.merge(other.description);
|
self.description.merge(other.description);
|
||||||
self.state.merge(other.state);
|
self.state.merge(other.state);
|
||||||
self.target.merge(other.target);
|
self.target.merge(other.target);
|
||||||
|
self.merges.merge(other.merges);
|
||||||
self.tags.merge(other.tags);
|
self.tags.merge(other.tags);
|
||||||
self.revisions.merge(other.revisions);
|
self.revisions.merge(other.revisions);
|
||||||
self.timeline.merge(other.timeline);
|
self.timeline.merge(other.timeline);
|
||||||
|
|
@ -184,6 +191,7 @@ impl Default for Patch {
|
||||||
state: LWWReg::initial(Max::from(State::default())),
|
state: LWWReg::initial(Max::from(State::default())),
|
||||||
target: LWWReg::initial(Max::from(MergeTarget::default())),
|
target: LWWReg::initial(Max::from(MergeTarget::default())),
|
||||||
tags: LWWSet::default(),
|
tags: LWWSet::default(),
|
||||||
|
merges: LWWMap::default(),
|
||||||
revisions: GMap::default(),
|
revisions: GMap::default(),
|
||||||
timeline: GSet::default(),
|
timeline: GSet::default(),
|
||||||
}
|
}
|
||||||
|
|
@ -197,8 +205,8 @@ impl Patch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Current state of the patch.
|
/// Current state of the patch.
|
||||||
pub fn state(&self) -> State {
|
pub fn state(&self) -> &State {
|
||||||
*self.state.get().get()
|
self.state.get().get()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Target this patch is meant to be merged in.
|
/// Target this patch is meant to be merged in.
|
||||||
|
|
@ -253,6 +261,13 @@ impl Patch {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the merges.
|
||||||
|
pub fn merges(&self) -> impl Iterator<Item = (&ActorId, &Merge)> {
|
||||||
|
self.merges
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(a, m)| m.get().map(|m| (a, m)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Reference to the Git object containing the code on the latest revision.
|
/// Reference to the Git object containing the code on the latest revision.
|
||||||
pub fn head(&self) -> &git::Oid {
|
pub fn head(&self) -> &git::Oid {
|
||||||
&self
|
&self
|
||||||
|
|
@ -287,7 +302,7 @@ impl Patch {
|
||||||
|
|
||||||
/// Check if the patch is open.
|
/// Check if the patch is open.
|
||||||
pub fn is_open(&self) -> bool {
|
pub fn is_open(&self) -> bool {
|
||||||
matches!(self.state(), State::Open)
|
matches!(self.state(), State::Open { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the patch is archived.
|
/// Check if the patch is archived.
|
||||||
|
|
@ -404,25 +419,56 @@ impl store::FromHistory for Patch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Action::Merge { revision, commit } => {
|
Action::Merge { revision, commit } => {
|
||||||
if let Some(Redactable::Present(revision)) = self.revisions.get_mut(&revision) {
|
if let Some(Redactable::Present(_)) = self.revisions.get_mut(&revision) {
|
||||||
revision.merges.insert(
|
let doc = repo.identity_doc_at(op.identity)?;
|
||||||
Merge {
|
if !doc.is_delegate(&op.author) {
|
||||||
node: op.author,
|
return Err(ApplyError::InvalidMerge(op.id));
|
||||||
|
}
|
||||||
|
self.merges.insert(
|
||||||
|
op.author,
|
||||||
|
Redactable::Present(Merge {
|
||||||
|
revision,
|
||||||
commit,
|
commit,
|
||||||
timestamp,
|
timestamp,
|
||||||
}
|
}),
|
||||||
.into(),
|
|
||||||
op.clock,
|
op.clock,
|
||||||
);
|
);
|
||||||
let doc = repo.identity_doc_at(op.identity)?;
|
|
||||||
|
|
||||||
if revision
|
let mut merges = self.merges.iter().fold(
|
||||||
.merges()
|
HashMap::<(RevisionId, git::Oid), usize>::new(),
|
||||||
.filter(|m| doc.is_delegate(&m.node))
|
|mut acc, (_, merge)| {
|
||||||
.count()
|
if let Some(merge) = merge.get() {
|
||||||
>= doc.threshold
|
*acc.entry((merge.revision, merge.commit)).or_default() += 1;
|
||||||
{
|
}
|
||||||
self.state.set(State::Merged, op.clock);
|
acc
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Discard revisions that weren't merged by a threshold of delegates.
|
||||||
|
merges.retain(|_, count| *count >= doc.threshold);
|
||||||
|
|
||||||
|
match merges.into_keys().collect::<Vec<_>>().as_slice() {
|
||||||
|
[] => {
|
||||||
|
// None of the revisions met the quorum.
|
||||||
|
}
|
||||||
|
[(revision, commit)] => {
|
||||||
|
// Patch is merged.
|
||||||
|
self.state.set(
|
||||||
|
State::Merged {
|
||||||
|
revision: *revision,
|
||||||
|
commit: *commit,
|
||||||
|
},
|
||||||
|
op.clock,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
revisions => {
|
||||||
|
// More than one revision met the quorum.
|
||||||
|
self.state.set(
|
||||||
|
State::Open {
|
||||||
|
conflicts: revisions.to_vec(),
|
||||||
|
},
|
||||||
|
op.clock,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(ApplyError::Missing(revision));
|
return Err(ApplyError::Missing(revision));
|
||||||
|
|
@ -466,8 +512,6 @@ pub struct Revision {
|
||||||
oid: git::Oid,
|
oid: git::Oid,
|
||||||
/// Discussion around this revision.
|
/// Discussion around this revision.
|
||||||
discussion: Thread,
|
discussion: Thread,
|
||||||
/// Merges of this revision into other repositories.
|
|
||||||
merges: LWWSet<Max<Merge>>,
|
|
||||||
/// Reviews of this revision's changes (one per actor).
|
/// Reviews of this revision's changes (one per actor).
|
||||||
reviews: GMap<ActorId, Review>,
|
reviews: GMap<ActorId, Review>,
|
||||||
/// When this revision was created.
|
/// When this revision was created.
|
||||||
|
|
@ -489,7 +533,6 @@ impl Revision {
|
||||||
base,
|
base,
|
||||||
oid,
|
oid,
|
||||||
discussion: Thread::default(),
|
discussion: Thread::default(),
|
||||||
merges: LWWSet::default(),
|
|
||||||
reviews: GMap::default(),
|
reviews: GMap::default(),
|
||||||
timestamp,
|
timestamp,
|
||||||
}
|
}
|
||||||
|
|
@ -524,11 +567,6 @@ impl Revision {
|
||||||
&self.discussion
|
&self.discussion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merges of this revision into other repositories.
|
|
||||||
pub fn merges(&self) -> impl Iterator<Item = &Merge> {
|
|
||||||
self.merges.iter().map(|m| m.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reviews of this revision's changes (one per actor).
|
/// Reviews of this revision's changes (one per actor).
|
||||||
pub fn reviews(&self) -> impl DoubleEndedIterator<Item = (&PublicKey, &Review)> {
|
pub fn reviews(&self) -> impl DoubleEndedIterator<Item = (&PublicKey, &Review)> {
|
||||||
self.reviews.iter()
|
self.reviews.iter()
|
||||||
|
|
@ -536,14 +574,29 @@ impl Revision {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Patch state.
|
/// Patch state.
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase", tag = "status")]
|
#[serde(rename_all = "camelCase", tag = "status")]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
#[default]
|
|
||||||
Open,
|
|
||||||
Draft,
|
Draft,
|
||||||
|
Open {
|
||||||
|
/// Revisions that were merged and are conflicting.
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
#[serde(default)]
|
||||||
|
conflicts: Vec<(RevisionId, git::Oid)>,
|
||||||
|
},
|
||||||
Archived,
|
Archived,
|
||||||
Merged,
|
Merged {
|
||||||
|
/// The revision that was merged.
|
||||||
|
revision: RevisionId,
|
||||||
|
/// The commit in the target branch that contains the changes.
|
||||||
|
commit: git::Oid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Open { conflicts: vec![] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for State {
|
impl fmt::Display for State {
|
||||||
|
|
@ -551,8 +604,8 @@ impl fmt::Display for State {
|
||||||
match self {
|
match self {
|
||||||
Self::Archived => write!(f, "archived"),
|
Self::Archived => write!(f, "archived"),
|
||||||
Self::Draft => write!(f, "draft"),
|
Self::Draft => write!(f, "draft"),
|
||||||
Self::Open => write!(f, "open"),
|
Self::Open { .. } => write!(f, "open"),
|
||||||
Self::Merged => write!(f, "merged"),
|
Self::Merged { .. } => write!(f, "merged"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -561,11 +614,11 @@ impl fmt::Display for State {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Merge {
|
pub struct Merge {
|
||||||
/// Owner of repository that this patch was merged into.
|
/// Revision that was merged.
|
||||||
pub node: NodeId,
|
pub revision: RevisionId,
|
||||||
/// Base branch commit that contains the revision.
|
/// Base branch commit that contains the revision.
|
||||||
pub commit: git::Oid,
|
pub commit: git::Oid,
|
||||||
/// When this merged was performed.
|
/// When this merge was performed.
|
||||||
pub timestamp: Timestamp,
|
pub timestamp: Timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1002,14 +1055,14 @@ impl<'a, 'g> PatchMut<'a, 'g> {
|
||||||
if !self.is_draft() {
|
if !self.is_draft() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
self.lifecycle(State::Open, signer)?;
|
self.lifecycle(State::Open { conflicts: vec![] }, signer)?;
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark an open patch as a draft. Returns `false` if the patch was not open.
|
/// Mark an open patch as a draft. Returns `false` if the patch was not open and free of merges.
|
||||||
pub fn unready<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
|
pub fn unready<G: Signer>(&mut self, signer: &G) -> Result<bool, Error> {
|
||||||
if !self.is_open() {
|
if !matches!(self.state(), State::Open { conflicts } if conflicts.is_empty()) {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
self.lifecycle(State::Draft, signer)?;
|
self.lifecycle(State::Draft, signer)?;
|
||||||
|
|
@ -1120,9 +1173,9 @@ impl<'a> Patches<'a> {
|
||||||
.fold(PatchCounts::default(), |mut state, (_, p, _)| {
|
.fold(PatchCounts::default(), |mut state, (_, p, _)| {
|
||||||
match p.state() {
|
match p.state() {
|
||||||
State::Draft => state.draft += 1,
|
State::Draft => state.draft += 1,
|
||||||
State::Open => state.open += 1,
|
State::Open { .. } => state.open += 1,
|
||||||
State::Archived => state.archived += 1,
|
State::Archived => state.archived += 1,
|
||||||
State::Merged => state.merged += 1,
|
State::Merged { .. } => state.merged += 1,
|
||||||
}
|
}
|
||||||
state
|
state
|
||||||
});
|
});
|
||||||
|
|
@ -1229,10 +1282,12 @@ mod test {
|
||||||
|
|
||||||
use radicle_crdt::test::{assert_laws, WeightedGenerator};
|
use radicle_crdt::test::{assert_laws, WeightedGenerator};
|
||||||
|
|
||||||
|
use nonempty::nonempty;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use qcheck::{Arbitrary, TestResult};
|
use qcheck::{Arbitrary, TestResult};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::assert_matches;
|
||||||
use crate::cob::test::Actor;
|
use crate::cob::test::Actor;
|
||||||
use crate::crypto::test::signer::MockSigner;
|
use crate::crypto::test::signer::MockSigner;
|
||||||
use crate::test;
|
use crate::test;
|
||||||
|
|
@ -1429,7 +1484,7 @@ mod test {
|
||||||
assert_eq!(patch.title(), "My first patch");
|
assert_eq!(patch.title(), "My first patch");
|
||||||
assert_eq!(patch.description(), "Blah blah blah.");
|
assert_eq!(patch.description(), "Blah blah blah.");
|
||||||
assert_eq!(patch.author().id(), &author);
|
assert_eq!(patch.author().id(), &author);
|
||||||
assert_eq!(patch.state(), State::Open);
|
assert_eq!(patch.state(), &State::Open { conflicts: vec![] });
|
||||||
assert_eq!(patch.target(), target);
|
assert_eq!(patch.target(), target);
|
||||||
assert_eq!(patch.version(), 0);
|
assert_eq!(patch.version(), 0);
|
||||||
|
|
||||||
|
|
@ -1504,13 +1559,63 @@ mod test {
|
||||||
|
|
||||||
let patch = patches.get(&id).unwrap().unwrap();
|
let patch = patches.get(&id).unwrap().unwrap();
|
||||||
|
|
||||||
let (_, r) = patch.revisions().next().unwrap();
|
let merges = patch.merges.iter().collect::<Vec<_>>();
|
||||||
let merges = r.merges.iter().collect::<Vec<_>>();
|
|
||||||
assert_eq!(merges.len(), 1);
|
assert_eq!(merges.len(), 1);
|
||||||
|
|
||||||
let merge = merges.first().unwrap();
|
let (merger, merge) = merges.first().unwrap();
|
||||||
assert_eq!(merge.node, *signer.public_key());
|
assert_eq!(*merger, signer.public_key());
|
||||||
assert_eq!(merge.commit, pr.base);
|
assert_eq!(merge.get().unwrap().commit, pr.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_patch_merge_and_archive() {
|
||||||
|
let rid = gen::<Id>(1);
|
||||||
|
let base = git::Oid::from_str("d8711a8d43dc919fe39ae4b7c2f7b24667f5d470").unwrap();
|
||||||
|
let commit = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
|
||||||
|
|
||||||
|
let mut alice = Actor::<MockSigner, _>::default();
|
||||||
|
let mut bob = Actor::<MockSigner, _>::default();
|
||||||
|
|
||||||
|
let proj = gen::<Project>(1);
|
||||||
|
let doc = Doc::new(proj, nonempty![alice.did(), bob.did()], 1)
|
||||||
|
.verified()
|
||||||
|
.unwrap();
|
||||||
|
let repo = MockRepository::new(rid, doc);
|
||||||
|
let patch = alice
|
||||||
|
.patch("Some changes", "", base, commit, &repo)
|
||||||
|
.unwrap();
|
||||||
|
let (revision, _) = patch.revisions().next().unwrap();
|
||||||
|
|
||||||
|
// Create two concurrent operations.
|
||||||
|
let clock = Lamport::from(2);
|
||||||
|
let identity = repo.identity_head().unwrap();
|
||||||
|
let ops = [
|
||||||
|
alice.op_with(
|
||||||
|
Action::Merge {
|
||||||
|
revision: *revision,
|
||||||
|
commit,
|
||||||
|
},
|
||||||
|
clock,
|
||||||
|
identity,
|
||||||
|
),
|
||||||
|
bob.op_with(
|
||||||
|
Action::Lifecycle {
|
||||||
|
state: State::Archived,
|
||||||
|
},
|
||||||
|
clock,
|
||||||
|
identity,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut patch1 = patch.clone();
|
||||||
|
let mut patch2 = patch.clone();
|
||||||
|
|
||||||
|
// Apply the ops in different orders and expect the patch state to remain the same.
|
||||||
|
patch1.apply(ops.iter().cloned(), &repo).unwrap();
|
||||||
|
patch2.apply(ops.iter().cloned().rev(), &repo).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(patch1.state(), &State::Merged { .. });
|
||||||
|
assert_matches!(patch2.state(), &State::Merged { .. });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1615,8 +1720,12 @@ mod test {
|
||||||
fn test_revision_merge_reinsert() {
|
fn test_revision_merge_reinsert() {
|
||||||
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
|
let base = git::Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap();
|
||||||
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
||||||
let repo = gen::<MockRepository>(1);
|
let id = gen::<Id>(1);
|
||||||
let mut alice = Actor::<_, Action>::new(MockSigner::default());
|
let mut alice = Actor::<_, Action>::new(MockSigner::default());
|
||||||
|
let mut doc = gen::<Doc<Verified>>(1);
|
||||||
|
doc.delegates.push(alice.signer.public_key().into());
|
||||||
|
let repo = MockRepository::new(id, doc);
|
||||||
|
|
||||||
let mut p1 = Patch::default();
|
let mut p1 = Patch::default();
|
||||||
let mut p2 = Patch::default();
|
let mut p2 = Patch::default();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,15 @@ use serde::Serialize;
|
||||||
|
|
||||||
use crate::cob::common::clock;
|
use crate::cob::common::clock;
|
||||||
use crate::cob::op::{Op, Ops};
|
use crate::cob::op::{Op, Ops};
|
||||||
|
use crate::cob::patch;
|
||||||
|
use crate::cob::patch::Patch;
|
||||||
use crate::cob::store::encoding;
|
use crate::cob::store::encoding;
|
||||||
use crate::cob::History;
|
use crate::cob::History;
|
||||||
use crate::crypto::{PublicKey, Signer};
|
use crate::crypto::{PublicKey, Signer};
|
||||||
|
use crate::git;
|
||||||
use crate::git::Oid;
|
use crate::git::Oid;
|
||||||
|
use crate::prelude::Did;
|
||||||
|
use crate::storage::ReadRepository;
|
||||||
use crate::test::arbitrary;
|
use crate::test::arbitrary;
|
||||||
|
|
||||||
use super::store::FromHistory;
|
use super::store::FromHistory;
|
||||||
|
|
@ -146,22 +151,62 @@ impl<G: Signer, A: Clone + Serialize> Actor<G, A> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new operation.
|
/// Create a new operation.
|
||||||
pub fn op(&mut self, action: A) -> Op<A> {
|
pub fn op_with(&mut self, action: A, clock: clock::Lamport, identity: Oid) -> Op<A> {
|
||||||
let id = arbitrary::oid().into();
|
let id = arbitrary::oid().into();
|
||||||
let author = *self.signer.public_key();
|
let author = *self.signer.public_key();
|
||||||
let clock = self.clock.tick();
|
|
||||||
let timestamp = clock::Physical::now();
|
let timestamp = clock::Physical::now();
|
||||||
let identity = arbitrary::oid();
|
|
||||||
let op = Op {
|
Op {
|
||||||
id,
|
id,
|
||||||
action,
|
action,
|
||||||
author,
|
author,
|
||||||
clock,
|
clock,
|
||||||
timestamp,
|
timestamp,
|
||||||
identity,
|
identity,
|
||||||
};
|
}
|
||||||
self.ops.insert((self.clock, author), op.clone());
|
}
|
||||||
|
|
||||||
|
/// Create a new operation.
|
||||||
|
pub fn op(&mut self, action: A) -> Op<A> {
|
||||||
|
let clock = self.clock.tick();
|
||||||
|
let identity = arbitrary::oid();
|
||||||
|
let op = self.op_with(action, clock, identity);
|
||||||
|
|
||||||
|
self.ops.insert((self.clock, op.author), op.clone());
|
||||||
|
|
||||||
op
|
op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the actor's DID.
|
||||||
|
pub fn did(&self) -> Did {
|
||||||
|
self.signer.public_key().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<G: Signer> Actor<G, super::patch::Action> {
|
||||||
|
/// Create a patch.
|
||||||
|
pub fn patch<R: ReadRepository>(
|
||||||
|
&mut self,
|
||||||
|
title: impl ToString,
|
||||||
|
description: impl ToString,
|
||||||
|
base: git::Oid,
|
||||||
|
oid: git::Oid,
|
||||||
|
repo: &R,
|
||||||
|
) -> Result<Patch, patch::ApplyError> {
|
||||||
|
Patch::from_ops(
|
||||||
|
[
|
||||||
|
self.op(patch::Action::Revision {
|
||||||
|
description: description.to_string(),
|
||||||
|
base,
|
||||||
|
oid,
|
||||||
|
}),
|
||||||
|
self.op(patch::Action::Edit {
|
||||||
|
title: title.to_string(),
|
||||||
|
description: description.to_string(),
|
||||||
|
target: patch::MergeTarget::default(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
repo,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ pub mod setup {
|
||||||
let oid = commit(&self.working, &refname, blobs, &[&parent]);
|
let oid = commit(&self.working, &refname, blobs, &[&parent]);
|
||||||
|
|
||||||
git::push(&self.working, &REMOTE_NAME, [(&refname, &refname)]).unwrap();
|
git::push(&self.working, &REMOTE_NAME, [(&refname, &refname)]).unwrap();
|
||||||
|
|
||||||
BranchWith {
|
BranchWith {
|
||||||
base: base.into(),
|
base: base.into(),
|
||||||
oid,
|
oid,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use git_ext::ref_format as fmt;
|
use git_ext::ref_format as fmt;
|
||||||
|
|
||||||
|
|
@ -222,11 +223,11 @@ impl ReadRepository for MockRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn identity_head(&self) -> Result<Oid, IdentityError> {
|
fn identity_head(&self) -> Result<Oid, IdentityError> {
|
||||||
todo!()
|
self.canonical_identity_head()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonical_identity_head(&self) -> Result<Oid, IdentityError> {
|
fn canonical_identity_head(&self) -> Result<Oid, IdentityError> {
|
||||||
todo!()
|
Ok(Oid::from_str("cb18e95ada2bb38aadd8e6cef0963ce37a87add3").unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue