Propagate rebase conflicts out from state object

This commit is contained in:
Magnus Hoff 2017-11-20 15:08:34 +01:00
parent bfca5d6e78
commit bf9716ccb8
4 changed files with 118 additions and 27 deletions

View file

@ -13,11 +13,22 @@ use self::output::Output::Resolved;
pub use self::output::Output; pub use self::output::Output;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum MergeResult<Item: Debug + PartialEq + Copy> { pub enum MergeResult<Item: Debug + PartialEq> {
Clean(String), Clean(String),
Conflicted(Vec<Output<Item>>), Conflicted(Vec<Output<Item>>),
} }
impl<'a> MergeResult<&'a str> {
pub fn to_strings(self) -> MergeResult<String> {
match self {
MergeResult::Clean(x) => MergeResult::Clean(x),
MergeResult::Conflicted(x) => MergeResult::Conflicted(
x.into_iter().map(Output::to_strings).collect()
)
}
}
}
pub fn merge_lines<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<&'a str> { pub fn merge_lines<'a>(a: &'a str, o: &'a str, b: &'a str) -> MergeResult<&'a str> {
let oa = diff::lines(o, a); let oa = diff::lines(o, a);
let ob = diff::lines(o, b); let ob = diff::lines(o, b);

View file

@ -6,11 +6,24 @@ use diff::Result::*;
use super::chunk::Chunk; use super::chunk::Chunk;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Output<Item: Debug + PartialEq + Copy> { pub enum Output<Item: Debug + PartialEq> {
Resolved(Vec<Item>), Resolved(Vec<Item>),
Conflict(Vec<Item>, Vec<Item>, Vec<Item>), Conflict(Vec<Item>, Vec<Item>, Vec<Item>),
} }
impl<'a> Output<&'a str> {
pub fn to_strings(self) -> Output<String> {
match self {
Output::Resolved(x) => Output::Resolved(x.into_iter().map(str::to_string).collect()),
Output::Conflict(a, o, b) => Output::Conflict(
a.into_iter().map(str::to_string).collect(),
o.into_iter().map(str::to_string).collect(),
b.into_iter().map(str::to_string).collect(),
),
}
}
}
fn choose_left<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> { fn choose_left<Item: Copy>(operations: &[diff::Result<Item>]) -> Vec<Item> {
operations operations
.iter() .iter()

View file

@ -148,6 +148,7 @@ impl Resource for ArticleResource {
self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity) self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity)
}) })
.and_then(|updated| { .and_then(|updated| {
let updated = updated.unwrap();
futures::finished(Response::new() futures::finished(Response::new()
.with_status(hyper::StatusCode::Ok) .with_status(hyper::StatusCode::Ok)
.with_header(ContentType(APPLICATION_JSON.clone())) .with_header(ContentType(APPLICATION_JSON.clone()))
@ -186,6 +187,7 @@ impl Resource for ArticleResource {
self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity) self.state.update_article(self.article_id, update.base_revision, update.title, update.body, identity)
}) })
.and_then(|updated| { .and_then(|updated| {
let updated = updated.unwrap();
futures::finished(Response::new() futures::finished(Response::new()
.with_status(hyper::StatusCode::SeeOther) .with_status(hyper::StatusCode::SeeOther)
.with_header(ContentType(TEXT_PLAIN.clone())) .with_header(ContentType(TEXT_PLAIN.clone()))

View file

@ -40,6 +40,34 @@ struct NewRevision<'a> {
latest: bool, latest: bool,
} }
#[derive(Debug, PartialEq)]
pub struct RebaseConflict {
base_revision: i32,
title: merge::MergeResult<char>,
body: merge::MergeResult<String>,
}
#[derive(Debug, PartialEq)]
enum RebaseResult {
Clean { title: String, body: String },
Conflict(RebaseConflict),
}
pub enum UpdateResult {
Success(models::ArticleRevision),
RebaseConflict(RebaseConflict),
}
impl UpdateResult {
// TODO Move to mod tests below
pub fn unwrap(self) -> models::ArticleRevision {
match self {
UpdateResult::Success(x) => x,
_ => panic!("Expected success")
}
}
}
fn decide_slug(conn: &SqliteConnection, article_id: i32, prev_title: &str, title: &str, prev_slug: Option<&str>) -> Result<String, Error> { fn decide_slug(conn: &SqliteConnection, article_id: i32, prev_title: &str, title: &str, prev_slug: Option<&str>) -> Result<String, Error> {
let base_slug = ::slug::slugify(title); let base_slug = ::slug::slugify(title);
@ -175,7 +203,7 @@ impl<'a> SyncState<'a> {
} }
fn rebase_update(&self, article_id: i32, target_base_revision: i32, existing_base_revision: i32, title: String, body: String) fn rebase_update(&self, article_id: i32, target_base_revision: i32, existing_base_revision: i32, title: String, body: String)
-> Result<(String, String), Error> -> Result<RebaseResult, Error>
{ {
let mut title_a = title; let mut title_a = title;
let mut body_a = body; let mut body_a = body;
@ -195,22 +223,33 @@ impl<'a> SyncState<'a> {
let (title_b, body_b) = stored.pop().expect("Application layer guarantee"); let (title_b, body_b) = stored.pop().expect("Application layer guarantee");
let (title_o, body_o) = stored.pop().expect("Application layer guarantee"); let (title_o, body_o) = stored.pop().expect("Application layer guarantee");
title_a = match merge::merge_chars(&title_a, &title_o, &title_b) { use merge::MergeResult::*;
merge::MergeResult::Clean(merged) => merged,
_ => unimplemented!("Missing handling of merge conflicts"), let update = {
let title_merge = merge::merge_chars(&title_a, &title_o, &title_b);
let body_merge = merge::merge_lines(&body_a, &body_o, &body_b);
match (title_merge, body_merge) {
(Clean(title), Clean(body)) => (title, body),
(title_merge, body_merge) => {
return Ok(RebaseResult::Conflict(RebaseConflict {
base_revision: revision,
title: title_merge,
body: body_merge.to_strings(),
}));
},
}
}; };
body_a = match merge::merge_lines(&body_a, &body_o, &body_b) { title_a = update.0;
merge::MergeResult::Clean(merged) => merged, body_a = update.1;
_ => unimplemented!("Missing handling of merge conflicts"),
};
} }
Ok((title_a, body_a)) Ok(RebaseResult::Clean { title: title_a, body: body_a })
} }
pub fn update_article(&self, article_id: i32, base_revision: i32, title: String, body: String, author: Option<String>) pub fn update_article(&self, article_id: i32, base_revision: i32, title: String, body: String, author: Option<String>)
-> Result<models::ArticleRevision, Error> -> Result<UpdateResult, Error>
{ {
if title.is_empty() { if title.is_empty() {
Err("title cannot be empty")?; Err("title cannot be empty")?;
@ -236,7 +275,12 @@ impl<'a> SyncState<'a> {
Err("This edit is based on a future version of the article")?; Err("This edit is based on a future version of the article")?;
} }
let (title, body) = self.rebase_update(article_id, latest_revision, base_revision, title, body)?; let rebase_result = self.rebase_update(article_id, latest_revision, base_revision, title, body)?;
let (title, body) = match rebase_result {
RebaseResult::Clean { title, body } => (title, body),
RebaseResult::Conflict(x) => return Ok(UpdateResult::RebaseConflict(x)),
};
let new_revision = latest_revision + 1; let new_revision = latest_revision + 1;
@ -262,11 +306,11 @@ impl<'a> SyncState<'a> {
.into(article_revisions::table) .into(article_revisions::table)
.execute(self.db_connection)?; .execute(self.db_connection)?;
Ok(article_revisions::table Ok(UpdateResult::Success(article_revisions::table
.filter(article_revisions::article_id.eq(article_id)) .filter(article_revisions::article_id.eq(article_id))
.filter(article_revisions::revision.eq(new_revision)) .filter(article_revisions::revision.eq(new_revision))
.first::<models::ArticleRevision>(self.db_connection)? .first::<models::ArticleRevision>(self.db_connection)?
) ))
}) })
} }
@ -409,7 +453,7 @@ impl State {
} }
pub fn update_article(&self, article_id: i32, base_revision: i32, title: String, body: String, author: Option<String>) pub fn update_article(&self, article_id: i32, base_revision: i32, title: String, body: String, author: Option<String>)
-> CpuFuture<models::ArticleRevision, Error> -> CpuFuture<UpdateResult, Error>
{ {
self.execute(move |state| state.update_article(article_id, base_revision, title, body, author)) self.execute(move |state| state.update_article(article_id, base_revision, title, body, author))
} }
@ -465,7 +509,7 @@ mod test {
let article = state.create_article(None, "Title".into(), "Body".into(), None).unwrap(); let article = state.create_article(None, "Title".into(), "Body".into(), None).unwrap();
let new_revision = state.update_article(article.article_id, article.revision, article.title.clone(), "New body".into(), None).unwrap(); let new_revision = state.update_article(article.article_id, article.revision, article.title.clone(), "New body".into(), None).unwrap().unwrap();
assert_eq!(article.article_id, new_revision.article_id); assert_eq!(article.article_id, new_revision.article_id);
@ -486,8 +530,8 @@ mod test {
let article = state.create_article(None, "Title".into(), "Body".into(), None).unwrap(); let article = state.create_article(None, "Title".into(), "Body".into(), None).unwrap();
let first_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "New body".into(), None).unwrap(); let first_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "New body".into(), None).unwrap().unwrap();
let second_edit = state.update_article(article.article_id, first_edit.revision, article.title.clone(), "Newer body".into(), None).unwrap(); let second_edit = state.update_article(article.article_id, first_edit.revision, article.title.clone(), "Newer body".into(), None).unwrap().unwrap();
assert_eq!("Newer body", second_edit.body); assert_eq!("Newer body", second_edit.body);
} }
@ -498,8 +542,8 @@ mod test {
let article = state.create_article(None, "Title".into(), "a\nb\nc\n".into(), None).unwrap(); let article = state.create_article(None, "Title".into(), "a\nb\nc\n".into(), None).unwrap();
let first_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "a\nx\nb\nc\n".into(), None).unwrap(); let first_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "a\nx\nb\nc\n".into(), None).unwrap().unwrap();
let second_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "a\nb\ny\nc\n".into(), None).unwrap(); let second_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "a\nb\ny\nc\n".into(), None).unwrap().unwrap();
assert!(article.revision < first_edit.revision); assert!(article.revision < first_edit.revision);
assert!(first_edit.revision < second_edit.revision); assert!(first_edit.revision < second_edit.revision);
@ -513,11 +557,11 @@ mod test {
let article = state.create_article(None, "Title".into(), "a\nb\nc\n".into(), None).unwrap(); let article = state.create_article(None, "Title".into(), "a\nb\nc\n".into(), None).unwrap();
let edit = state.update_article(article.article_id, article.revision, article.title.clone(), "a\nx1\nb\nc\n".into(), None).unwrap(); let edit = state.update_article(article.article_id, article.revision, article.title.clone(), "a\nx1\nb\nc\n".into(), None).unwrap().unwrap();
let edit = state.update_article(article.article_id, edit.revision, article.title.clone(), "a\nx1\nx2\nb\nc\n".into(), None).unwrap(); let edit = state.update_article(article.article_id, edit.revision, article.title.clone(), "a\nx1\nx2\nb\nc\n".into(), None).unwrap().unwrap();
let edit = state.update_article(article.article_id, edit.revision, article.title.clone(), "a\nx1\nx2\nx3\nb\nc\n".into(), None).unwrap(); let edit = state.update_article(article.article_id, edit.revision, article.title.clone(), "a\nx1\nx2\nx3\nb\nc\n".into(), None).unwrap().unwrap();
let rebase_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "a\nb\ny\nc\n".into(), None).unwrap(); let rebase_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "a\nb\ny\nc\n".into(), None).unwrap().unwrap();
assert!(article.revision < edit.revision); assert!(article.revision < edit.revision);
assert!(edit.revision < rebase_edit.revision); assert!(edit.revision < rebase_edit.revision);
@ -531,12 +575,33 @@ mod test {
let article = state.create_article(None, "titlle".into(), "".into(), None).unwrap(); let article = state.create_article(None, "titlle".into(), "".into(), None).unwrap();
let first_edit = state.update_article(article.article_id, article.revision, "Titlle".into(), article.body.clone(), None).unwrap(); let first_edit = state.update_article(article.article_id, article.revision, "Titlle".into(), article.body.clone(), None).unwrap().unwrap();
let second_edit = state.update_article(article.article_id, article.revision, "title".into(), article.body.clone(), None).unwrap(); let second_edit = state.update_article(article.article_id, article.revision, "title".into(), article.body.clone(), None).unwrap().unwrap();
assert!(article.revision < first_edit.revision); assert!(article.revision < first_edit.revision);
assert!(first_edit.revision < second_edit.revision); assert!(first_edit.revision < second_edit.revision);
assert_eq!("Title", second_edit.title); assert_eq!("Title", second_edit.title);
} }
#[test]
fn update_article_when_merge_conflict() {
init!(state);
let article = state.create_article(None, "Title".into(), "a".into(), None).unwrap();
state.update_article(article.article_id, article.revision, article.title.clone(), "b".into(), None).unwrap().unwrap();
let conflict_edit = state.update_article(article.article_id, article.revision, article.title.clone(), "c".into(), None).unwrap();
match conflict_edit {
UpdateResult::Success(..) => panic!("Expected conflict"),
UpdateResult::RebaseConflict(RebaseConflict { base_revision, title, body }) => {
assert_eq!(article.revision, base_revision);
assert_eq!(title, merge::MergeResult::Clean(article.title.clone()));
assert_eq!(body, merge::MergeResult::Conflicted(vec![
merge::Output::Conflict(vec!["c"], vec!["a"], vec!["b"]),
]).to_strings());
}
};
}
} }