1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2024-11-22 18:51:08 +00:00

conflict moved to db

This commit is contained in:
Felix Ableitner 2023-12-05 01:17:02 +01:00
parent 37352a3e86
commit 5d2099c17c
19 changed files with 342 additions and 254 deletions

14
Cargo.lock generated
View file

@ -492,6 +492,7 @@ dependencies = [
"diesel_derives", "diesel_derives",
"itoa", "itoa",
"pq-sys", "pq-sys",
"uuid",
] ]
[[package]] [[package]]
@ -679,6 +680,7 @@ dependencies = [
"enum_delegate", "enum_delegate",
"env_logger", "env_logger",
"futures", "futures",
"hex",
"once_cell", "once_cell",
"pretty_assertions", "pretty_assertions",
"rand", "rand",
@ -688,6 +690,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"url", "url",
"uuid",
] ]
[[package]] [[package]]
@ -924,6 +927,12 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.11" version = "0.2.11"
@ -2276,11 +2285,12 @@ dependencies = [
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.5.0" version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"serde",
] ]
[[package]] [[package]]

View file

@ -10,19 +10,21 @@ async-trait = "0.1.74"
axum = "0.6.20" axum = "0.6.20"
axum-macros = "0.3.8" axum-macros = "0.3.8"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
diesel = {version = "2.1.4", features = ["postgres", "chrono"] } diesel = {version = "2.1.4", features = ["postgres", "chrono", "uuid"] }
diesel-derive-newtype = "2.1.0" diesel-derive-newtype = "2.1.0"
diesel_migrations = "2.1.0" diesel_migrations = "2.1.0"
diffy = "0.3.0" diffy = "0.3.0"
enum_delegate = "0.2.0" enum_delegate = "0.2.0"
env_logger = { version = "0.10.1", default-features = false } env_logger = { version = "0.10.1", default-features = false }
futures = "0.3.29" futures = "0.3.29"
hex = "0.4.3"
rand = "0.8.5" rand = "0.8.5"
serde = "1.0.192" serde = "1.0.192"
sha2 = "0.10.8" sha2 = "0.10.8"
tokio = { version = "1.34.0", features = ["full"] } tokio = { version = "1.34.0", features = ["full"] }
tracing = "0.1.40" tracing = "0.1.40"
url = "2.4.1" url = "2.4.1"
uuid = { version = "1.6.1", features = ["serde"] }
[dev-dependencies] [dev-dependencies]
once_cell = "1.18.0" once_cell = "1.18.0"

View file

@ -1,3 +1,4 @@
drop table conflict;
drop table edit; drop table edit;
drop table article; drop table article;
drop table instance_follow; drop table instance_follow;

View file

@ -28,9 +28,16 @@ create table article (
create table edit ( create table edit (
id serial primary key, id serial primary key,
hash uuid not null,
ap_id varchar(255) not null unique, ap_id varchar(255) not null unique,
diff text not null, diff text not null,
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
version text not null, previous_version_id uuid not null
previous_version text not null );
)
create table conflict (
id uuid primary key,
diff text not null,
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
previous_version_id uuid not null
);

View file

@ -1,7 +1,9 @@
use crate::database::article::{ArticleView, DbArticle, DbArticleForm}; use crate::database::article::{ArticleView, DbArticle, DbArticleForm};
use crate::database::edit::{DbEdit, EditVersion}; use crate::database::conflict::{ApiConflict, DbConflict, DbConflictForm};
use crate::database::edit::{DbEdit, DbEditForm};
use crate::database::instance::{DbInstance, InstanceView}; use crate::database::instance::{DbInstance, InstanceView};
use crate::database::{DbConflict, MyDataHandle}; use crate::database::version::EditVersion;
use crate::database::MyDataHandle;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::activities::create_article::CreateArticle; use crate::federation::activities::create_article::CreateArticle;
use crate::federation::activities::follow::Follow; use crate::federation::activities::follow::Follow;
@ -9,14 +11,12 @@ use crate::federation::activities::submit_article_update;
use crate::utils::generate_article_version; use crate::utils::generate_article_version;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
use anyhow::anyhow;
use axum::extract::Query; use axum::extract::Query;
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::{Form, Json, Router}; use axum::{Form, Json, Router};
use axum_macros::debug_handler; use axum_macros::debug_handler;
use diffy::create_patch; use diffy::create_patch;
use futures::future::try_join_all; use futures::future::try_join_all;
use rand::random;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -60,8 +60,7 @@ async fn create_article(
instance_id: local_instance.id, instance_id: local_instance.id,
local: true, local: true,
}; };
dbg!(&form.ap_id); let article = DbArticle::create(&form, &data.db_connection)?;
let article = dbg!(DbArticle::create(&form, &data.db_connection))?;
CreateArticle::send_to_followers(article.clone(), &data).await?; CreateArticle::send_to_followers(article.clone(), &data).await?;
@ -77,17 +76,9 @@ pub struct EditArticleData {
pub new_text: String, pub new_text: String,
/// The version that this edit is based on, ie [DbArticle.latest_version] or /// The version that this edit is based on, ie [DbArticle.latest_version] or
/// [ApiConflict.previous_version] /// [ApiConflict.previous_version]
pub previous_version: EditVersion, pub previous_version_id: EditVersion,
/// If you are resolving a conflict, pass the id to delete conflict from the database /// If you are resolving a conflict, pass the id to delete conflict from the database
pub resolve_conflict_id: Option<i32>, pub resolve_conflict_id: Option<EditVersion>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ApiConflict {
pub id: i32,
pub three_way_merge: String,
pub article_id: ObjectId<DbArticle>,
pub previous_version: EditVersion,
} }
/// Edit an existing article (local or remote). /// Edit an existing article (local or remote).
@ -105,21 +96,17 @@ async fn edit_article(
Form(edit_form): Form<EditArticleData>, Form(edit_form): Form<EditArticleData>,
) -> MyResult<Json<Option<ApiConflict>>> { ) -> MyResult<Json<Option<ApiConflict>>> {
// resolve conflict if any // resolve conflict if any
if let Some(resolve_conflict_id) = &edit_form.resolve_conflict_id { if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
let mut lock = data.conflicts.lock().unwrap(); DbConflict::delete(resolve_conflict_id, &data.db_connection)?;
if !lock.iter().any(|c| &c.id == resolve_conflict_id) {
return Err(anyhow!("invalid resolve conflict"))?;
}
lock.retain(|c| &c.id != resolve_conflict_id);
} }
let original_article = DbArticle::read_view(edit_form.article_id, &data.db_connection)?; let original_article = DbArticle::read_view(edit_form.article_id, &data.db_connection)?;
if edit_form.previous_version == original_article.latest_version { if edit_form.previous_version_id == original_article.latest_version {
// No intermediate changes, simply submit new version // No intermediate changes, simply submit new version
submit_article_update( submit_article_update(
&data, &data,
edit_form.new_text.clone(), edit_form.new_text.clone(),
edit_form.previous_version, edit_form.previous_version_id,
&original_article.article, &original_article.article,
) )
.await?; .await?;
@ -128,20 +115,18 @@ async fn edit_article(
// There have been other changes since this edit was initiated. Get the common ancestor // There have been other changes since this edit was initiated. Get the common ancestor
// version and generate a diff to find out what exactly has changed. // version and generate a diff to find out what exactly has changed.
let ancestor = let ancestor =
generate_article_version(&original_article.edits, &edit_form.previous_version)?; generate_article_version(&original_article.edits, &edit_form.previous_version_id)?;
let patch = create_patch(&ancestor, &edit_form.new_text); let patch = create_patch(&ancestor, &edit_form.new_text);
let db_conflict = DbConflict { let previous_version = DbEdit::read(&edit_form.previous_version_id, &data.db_connection)?;
id: random(), let form = DbConflictForm {
id: EditVersion::new(&patch.to_string())?,
diff: patch.to_string(), diff: patch.to_string(),
article_id: original_article.article.ap_id.clone(), article_id: original_article.article.id,
previous_version: edit_form.previous_version, previous_version_id: previous_version.hash,
}; };
{ let conflict = DbConflict::create(&form, &data.db_connection)?;
let mut lock = data.conflicts.lock().unwrap(); Ok(Json(conflict.to_api_conflict(&data).await?))
lock.push(db_conflict.clone());
}
Ok(Json(db_conflict.to_api_conflict(&data).await?))
} }
} }
@ -186,8 +171,8 @@ async fn resolve_article(
data: Data<MyDataHandle>, data: Data<MyDataHandle>,
) -> MyResult<Json<ArticleView>> { ) -> MyResult<Json<ArticleView>> {
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?; let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
let edits = DbEdit::for_article(&article, &data.db_connection)?; let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
let latest_version = edits.last().unwrap().version.clone(); let latest_version = edits.last().unwrap().hash.clone();
Ok(Json(ArticleView { Ok(Json(ArticleView {
article, article,
edits, edits,
@ -226,7 +211,7 @@ async fn follow_instance(
/// Get a list of all unresolved edit conflicts. /// Get a list of all unresolved edit conflicts.
#[debug_handler] #[debug_handler]
async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> { async fn edit_conflicts(data: Data<MyDataHandle>) -> MyResult<Json<Vec<ApiConflict>>> {
let conflicts = { data.conflicts.lock().unwrap().to_vec() }; let conflicts = DbConflict::list(&data.db_connection)?;
let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| { let conflicts: Vec<ApiConflict> = try_join_all(conflicts.into_iter().map(|c| {
let data = data.reset_request_count(); let data = data.reset_request_count();
async move { c.to_api_conflict(&data).await } async move { c.to_api_conflict(&data).await }
@ -289,10 +274,18 @@ async fn fork_article(
// copy edits to new article // copy edits to new article
// TODO: convert to sql // TODO: convert to sql
let edits = DbEdit::for_article(&original_article, &data.db_connection)?; let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?;
for e in edits { for e in edits {
let form = e.copy_to_local_fork(&article)?; let ap_id = DbEditForm::generate_ap_id(&article, &e.hash)?;
DbEdit::create(&form, &data.db_connection)?; // TODO: id gives db unique violation
let form = DbEditForm {
ap_id,
diff: e.diff,
article_id: article.id,
hash: e.hash,
previous_version_id: e.previous_version_id,
};
dbg!(DbEdit::create(&form, &data.db_connection))?;
} }
CreateArticle::send_to_followers(article.clone(), &data).await?; CreateArticle::send_to_followers(article.clone(), &data).await?;

View file

@ -1,4 +1,4 @@
use crate::database::edit::{DbEdit, EditVersion}; use crate::database::edit::DbEdit;
use crate::database::schema::article; use crate::database::schema::article;
use crate::error::MyResult; use crate::error::MyResult;
@ -6,7 +6,7 @@ use crate::federation::objects::edits_collection::DbEditCollection;
use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::fetch::collection_id::CollectionId;
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
use diesel::pg::PgConnection; use diesel::pg::PgConnection;
use diesel::BelongingToDsl;
use diesel::ExpressionMethods; use diesel::ExpressionMethods;
use diesel::{ use diesel::{
insert_into, AsChangeset, BoolExpressionMethods, Identifiable, Insertable, insert_into, AsChangeset, BoolExpressionMethods, Identifiable, Insertable,
@ -14,6 +14,7 @@ use diesel::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::database::version::EditVersion;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::sync::Mutex; use std::sync::Mutex;
@ -86,8 +87,7 @@ impl DbArticle {
article::table.find(id).get_result(conn.deref_mut())? article::table.find(id).get_result(conn.deref_mut())?
}; };
let latest_version = article.latest_edit_version(conn)?; let latest_version = article.latest_edit_version(conn)?;
let mut conn = conn.lock().unwrap(); let edits: Vec<DbEdit> = DbEdit::read_for_article(&article, conn)?;
let edits: Vec<DbEdit> = DbEdit::belonging_to(&article).get_results(conn.deref_mut())?;
Ok(ArticleView { Ok(ArticleView {
article, article,
edits, edits,
@ -137,9 +137,8 @@ impl DbArticle {
// TODO: shouldnt have to read all edits from db // TODO: shouldnt have to read all edits from db
pub fn latest_edit_version(&self, conn: &Mutex<PgConnection>) -> MyResult<EditVersion> { pub fn latest_edit_version(&self, conn: &Mutex<PgConnection>) -> MyResult<EditVersion> {
let mut conn = conn.lock().unwrap(); let edits: Vec<DbEdit> = DbEdit::read_for_article(self, conn)?;
let edits: Vec<DbEdit> = DbEdit::belonging_to(&self).get_results(conn.deref_mut())?; match edits.last().map(|e| e.hash.clone()) {
match edits.last().map(|e| e.version.clone()) {
Some(latest_version) => Ok(latest_version), Some(latest_version) => Ok(latest_version),
None => Ok(EditVersion::default()), None => Ok(EditVersion::default()),
} }

108
src/database/conflict.rs Normal file
View file

@ -0,0 +1,108 @@
use crate::database::article::DbArticle;
use crate::database::edit::DbEdit;
use crate::database::schema::conflict;
use crate::database::version::EditVersion;
use crate::database::MyDataHandle;
use crate::error::MyResult;
use crate::federation::activities::submit_article_update;
use crate::utils::generate_article_version;
use activitypub_federation::config::Data;
use diesel::{
delete, insert_into, Identifiable, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl,
Selectable,
};
use diffy::{apply, merge, Patch};
use serde::{Deserialize, Serialize};
use std::ops::DerefMut;
use std::sync::Mutex;
/// A local only object which represents a merge conflict. It is created
/// when a local user edit conflicts with another concurrent edit.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)]
#[diesel(table_name = conflict, check_for_backend(diesel::pg::Pg), belongs_to(DbArticle, foreign_key = article_id))]
pub struct DbConflict {
pub id: EditVersion,
pub diff: String,
pub article_id: i32,
pub previous_version_id: EditVersion,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct ApiConflict {
pub id: EditVersion,
pub three_way_merge: String,
pub article_id: i32,
pub previous_version_id: EditVersion,
}
#[derive(Debug, Clone, Insertable)]
#[diesel(table_name = conflict, check_for_backend(diesel::pg::Pg))]
pub struct DbConflictForm {
pub id: EditVersion,
pub diff: String,
pub article_id: i32,
pub previous_version_id: EditVersion,
}
impl DbConflict {
pub fn create(form: &DbConflictForm, conn: &Mutex<PgConnection>) -> MyResult<Self> {
let mut conn = conn.lock().unwrap();
Ok(insert_into(conflict::table)
.values(form)
.get_result(conn.deref_mut())?)
}
pub fn list(conn: &Mutex<PgConnection>) -> MyResult<Vec<Self>> {
let mut conn = conn.lock().unwrap();
Ok(conflict::table.get_results(conn.deref_mut())?)
}
/// Delete a merge conflict after it is resolved.
pub fn delete(id: EditVersion, conn: &Mutex<PgConnection>) -> MyResult<Self> {
let mut conn = conn.lock().unwrap();
// TODO: should throw error on invalid id param
Ok(delete(conflict::table.find(id)).get_result(conn.deref_mut())?)
}
pub async fn to_api_conflict(
&self,
data: &Data<MyDataHandle>,
) -> MyResult<Option<ApiConflict>> {
let article = DbArticle::read(self.article_id, &data.db_connection)?;
// Make sure to get latest version from origin so that all conflicts can be resolved
let original_article = article.ap_id.dereference_forced(data).await?;
// create common ancestor version
let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?;
let ancestor = generate_article_version(&edits, &self.previous_version_id)?;
let patch = Patch::from_str(&self.diff)?;
// apply self.diff to ancestor to get `ours`
let ours = apply(&ancestor, &patch)?;
match merge(&ancestor, &ours, &original_article.text) {
Ok(new_text) => {
// patch applies cleanly so we are done
// federate the change
submit_article_update(
data,
new_text,
self.previous_version_id.clone(),
&original_article,
)
.await?;
DbConflict::delete(self.id.clone(), &data.db_connection)?;
Ok(None)
}
Err(three_way_merge) => {
// there is a merge conflict, user needs to do three-way-merge
Ok(Some(ApiConflict {
id: self.id.clone(),
three_way_merge,
article_id: original_article.id,
previous_version_id: original_article
.latest_edit_version(&data.db_connection)?,
}))
}
}
}
}

View file

@ -1,80 +1,71 @@
use crate::database::schema::edit; use crate::database::schema::edit;
use crate::database::version::EditVersion;
use crate::database::DbArticle; use crate::database::DbArticle;
use crate::error::MyResult; use crate::error::MyResult;
use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::fetch::object_id::ObjectId;
use diesel::ExpressionMethods;
use diesel::{ use diesel::{
insert_into, AsChangeset, Identifiable, Insertable, PgConnection, Queryable, RunQueryDsl, insert_into, AsChangeset, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl,
Selectable, Selectable,
}; };
use diesel::{Associations, BelongingToDsl};
use diesel_derive_newtype::DieselNewType;
use diffy::create_patch; use diffy::create_patch;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha224};
use std::ops::DerefMut; use std::ops::DerefMut;
use std::sync::Mutex; use std::sync::Mutex;
/// Represents a single change to the article. /// Represents a single change to the article.
#[derive( #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable)]
Clone, #[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
Debug,
Serialize,
Deserialize,
PartialEq,
Queryable,
Selectable,
Identifiable,
Associations,
)]
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg), belongs_to(DbArticle, foreign_key = article_id))]
pub struct DbEdit { pub struct DbEdit {
// TODO: we could use hash as primary key, but that gives errors on forking because
// the same edit is used for multiple articles
pub id: i32, pub id: i32,
/// UUID built from sha224 hash of diff
pub hash: EditVersion,
pub ap_id: ObjectId<DbEdit>, pub ap_id: ObjectId<DbEdit>,
pub diff: String, pub diff: String,
pub article_id: i32, pub article_id: i32,
pub version: EditVersion, /// First edit of an article always has `EditVersion::default()` here
// TODO: could be an Option<DbEdit.id> instead pub previous_version_id: EditVersion,
pub previous_version: EditVersion,
} }
#[derive(Debug, Clone, Insertable, AsChangeset)] #[derive(Debug, Clone, Insertable, AsChangeset)]
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))] #[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
pub struct DbEditForm { pub struct DbEditForm {
pub hash: EditVersion,
pub ap_id: ObjectId<DbEdit>, pub ap_id: ObjectId<DbEdit>,
pub diff: String, pub diff: String,
pub article_id: i32, pub article_id: i32,
pub version: EditVersion, pub previous_version_id: EditVersion,
pub previous_version: EditVersion,
} }
impl DbEditForm { impl DbEditForm {
pub fn new( pub fn new(
original_article: &DbArticle, original_article: &DbArticle,
updated_text: &str, updated_text: &str,
previous_version: EditVersion, previous_version_id: EditVersion,
) -> MyResult<Self> { ) -> MyResult<Self> {
let diff = create_patch(&original_article.text, updated_text); let diff = create_patch(&original_article.text, updated_text);
let (ap_id, hash) = Self::generate_ap_id_and_hash(original_article, diff.to_bytes())?; let version = EditVersion::new(&diff.to_string())?;
let ap_id = Self::generate_ap_id(original_article, &version)?;
Ok(DbEditForm { Ok(DbEditForm {
hash: version,
ap_id, ap_id,
diff: diff.to_string(), diff: diff.to_string(),
article_id: original_article.id, article_id: original_article.id,
version: EditVersion(hash), previous_version_id,
previous_version,
}) })
} }
fn generate_ap_id_and_hash( pub(crate) fn generate_ap_id(
article: &DbArticle, article: &DbArticle,
diff: Vec<u8>, version: &EditVersion,
) -> MyResult<(ObjectId<DbEdit>, String)> { ) -> MyResult<ObjectId<DbEdit>> {
let mut sha224 = Sha224::new(); Ok(ObjectId::parse(&format!(
sha224.update(diff); "{}/{}",
let hash = format!("{:X}", sha224.finalize()); article.ap_id,
Ok(( version.hash()
ObjectId::parse(&format!("{}/{}", article.ap_id, hash))?, ))?)
hash,
))
} }
} }
@ -88,31 +79,20 @@ impl DbEdit {
.set(form) .set(form)
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn read(version: &EditVersion, conn: &Mutex<PgConnection>) -> MyResult<Self> {
pub fn for_article(article: &DbArticle, conn: &Mutex<PgConnection>) -> MyResult<Vec<Self>> {
let mut conn = conn.lock().unwrap(); let mut conn = conn.lock().unwrap();
Ok(DbEdit::belonging_to(&article).get_results(conn.deref_mut())?) Ok(edit::table
} .filter(edit::dsl::hash.eq(version))
pub fn copy_to_local_fork(self, article: &DbArticle) -> MyResult<DbEditForm> { .get_result(conn.deref_mut())?)
let (ap_id, _) =
DbEditForm::generate_ap_id_and_hash(article, self.diff.clone().into_bytes())?;
Ok(DbEditForm {
ap_id,
diff: self.diff,
article_id: article.id,
version: self.version,
previous_version: self.previous_version,
})
}
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, DieselNewType)] pub fn read_for_article(
pub struct EditVersion(pub String); article: &DbArticle,
conn: &Mutex<PgConnection>,
impl Default for EditVersion { ) -> MyResult<Vec<Self>> {
fn default() -> Self { let mut conn = conn.lock().unwrap();
let sha224 = Sha224::new(); Ok(edit::table
let hash = format!("{:X}", sha224.finalize()); .filter(edit::dsl::article_id.eq(article.id))
EditVersion(hash) .get_results(conn.deref_mut())?)
} }
} }

View file

@ -1,90 +1,27 @@
use crate::api::ApiConflict;
use crate::database::article::DbArticle; use crate::database::article::DbArticle;
use crate::database::edit::DbEdit;
use crate::error::MyResult;
use crate::federation::activities::submit_article_update;
use crate::utils::generate_article_version;
use activitypub_federation::config::Data;
use activitypub_federation::fetch::object_id::ObjectId;
use diesel::PgConnection;
use diffy::{apply, merge, Patch};
use edit::EditVersion;
use diesel::PgConnection;
use std::ops::Deref; use std::ops::Deref;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
pub mod article; pub mod article;
pub mod conflict;
pub mod edit; pub mod edit;
pub mod instance; pub mod instance;
mod schema; mod schema;
pub mod version;
#[derive(Clone)] #[derive(Clone)]
pub struct MyData { pub struct MyData {
pub db_connection: Arc<Mutex<PgConnection>>, pub db_connection: Arc<Mutex<PgConnection>>,
pub fake_db: Arc<FakeDatabase>,
} }
impl Deref for MyData { impl Deref for MyData {
type Target = Arc<FakeDatabase>; type Target = Arc<Mutex<PgConnection>>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.fake_db &self.db_connection
} }
} }
pub type MyDataHandle = MyData; pub type MyDataHandle = MyData;
pub struct FakeDatabase {
pub conflicts: Mutex<Vec<DbConflict>>,
}
#[derive(Clone, Debug)]
pub struct DbConflict {
pub id: i32,
pub diff: String,
pub article_id: ObjectId<DbArticle>,
pub previous_version: EditVersion,
}
impl DbConflict {
pub async fn to_api_conflict(
&self,
data: &Data<MyDataHandle>,
) -> MyResult<Option<ApiConflict>> {
// Make sure to get latest version from origin so that all conflicts can be resolved
let original_article = self.article_id.dereference_forced(&data).await?;
// create common ancestor version
let edits = DbEdit::for_article(&original_article, &data.db_connection)?;
let ancestor = generate_article_version(&edits, &self.previous_version)?;
let patch = Patch::from_str(&self.diff)?;
// apply self.diff to ancestor to get `ours`
let ours = apply(&ancestor, &patch)?;
match dbg!(merge(&ancestor, &ours, &original_article.text)) {
Ok(new_text) => {
// patch applies cleanly so we are done
// federate the change
submit_article_update(
data,
new_text,
self.previous_version.clone(),
&original_article,
)
.await?;
// remove conflict from db
let mut lock = data.conflicts.lock().unwrap();
lock.retain(|c| c.id != self.id);
Ok(None)
}
Err(three_way_merge) => {
// there is a merge conflict, user needs to do three-way-merge
Ok(Some(ApiConflict {
id: self.id,
three_way_merge,
article_id: original_article.ap_id.clone(),
previous_version: original_article.latest_edit_version(&data.db_connection)?,
}))
}
}
}
}

View file

@ -12,15 +12,24 @@ diesel::table! {
} }
} }
diesel::table! {
conflict (id) {
id -> Uuid,
diff -> Text,
article_id -> Int4,
previous_version_id -> Uuid,
}
}
diesel::table! { diesel::table! {
edit (id) { edit (id) {
id -> Int4, id -> Int4,
hash -> Uuid,
#[max_length = 255] #[max_length = 255]
ap_id -> Varchar, ap_id -> Varchar,
diff -> Text, diff -> Text,
article_id -> Int4, article_id -> Int4,
version -> Text, previous_version_id -> Uuid,
previous_version -> Text,
} }
} }
@ -49,6 +58,7 @@ diesel::table! {
} }
diesel::joinable!(article -> instance (instance_id)); diesel::joinable!(article -> instance (instance_id));
diesel::joinable!(conflict -> article (article_id));
diesel::joinable!(edit -> article (article_id)); diesel::joinable!(edit -> article (article_id));
diesel::allow_tables_to_appear_in_same_query!(article, edit, instance, instance_follow,); diesel::allow_tables_to_appear_in_same_query!(article, conflict, edit, instance, instance_follow,);

43
src/database/version.rs Normal file
View file

@ -0,0 +1,43 @@
use crate::error::MyResult;
use std::hash::Hash;
use diesel_derive_newtype::DieselNewType;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use uuid::Uuid;
/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff
/// and using the first 16 bytes so that it fits into UUID.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, DieselNewType)]
pub struct EditVersion(Uuid);
impl EditVersion {
pub fn new(diff: &str) -> MyResult<Self> {
let mut sha256 = Sha256::new();
sha256.update(diff);
let hash_bytes = sha256.finalize();
let uuid = Uuid::from_slice(&hash_bytes.as_slice()[..16])?;
Ok(EditVersion(uuid))
}
pub fn hash(&self) -> String {
hex::encode(self.0.into_bytes())
}
}
impl Default for EditVersion {
fn default() -> Self {
EditVersion::new("").unwrap()
}
}
#[test]
fn test_edit_versions() -> MyResult<()> {
let default = EditVersion::default();
assert_eq!("e3b0c44298fc1c149afbf4c8996fb924", default.hash());
let version = EditVersion::new("test")?;
assert_eq!("9f86d081884c7d659a2feaa0c55ad015", version.hash());
Ok(())
}

View file

@ -1,6 +1,7 @@
use crate::database::article::DbArticle; use crate::database::article::DbArticle;
use crate::database::edit::{DbEdit, DbEditForm, EditVersion}; use crate::database::edit::{DbEdit, DbEditForm};
use crate::database::instance::DbInstance; use crate::database::instance::DbInstance;
use crate::database::version::EditVersion;
use crate::database::MyDataHandle; use crate::database::MyDataHandle;
use crate::error::Error; use crate::error::Error;
use crate::federation::activities::update_local_article::UpdateLocalArticle; use crate::federation::activities::update_local_article::UpdateLocalArticle;
@ -30,12 +31,12 @@ pub async fn submit_article_update(
} else { } else {
// dont insert edit into db, might be invalid in case of conflict // dont insert edit into db, might be invalid in case of conflict
let edit = DbEdit { let edit = DbEdit {
id: 0, id: -1,
hash: form.hash,
ap_id: form.ap_id, ap_id: form.ap_id,
diff: form.diff, diff: form.diff,
article_id: form.article_id, article_id: form.article_id,
version: form.version, previous_version_id: form.previous_version_id,
previous_version: form.previous_version,
}; };
let instance = DbInstance::read(original_article.instance_id, &data.db_connection)?; let instance = DbInstance::read(original_article.instance_id, &data.db_connection)?;
UpdateRemoteArticle::send(edit, instance, data).await?; UpdateRemoteArticle::send(edit, instance, data).await?;

View file

@ -1,4 +1,6 @@
use crate::database::conflict::{DbConflict, DbConflictForm};
use crate::database::instance::DbInstance; use crate::database::instance::DbInstance;
use crate::database::version::EditVersion;
use crate::database::MyDataHandle; use crate::database::MyDataHandle;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::objects::edit::ApubEdit; use crate::federation::objects::edit::ApubEdit;
@ -8,11 +10,7 @@ use activitypub_federation::{
config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many,
traits::ActivityHandler, traits::ActivityHandler,
}; };
use rand::random;
use crate::database::article::DbArticle;
use crate::database::DbConflict;
use crate::federation::activities::update_local_article::UpdateLocalArticle;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -68,16 +66,15 @@ impl ActivityHandler for RejectEdit {
} }
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> { async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
dbg!(&self);
// cant convert this to DbEdit as it tries to apply patch and fails // cant convert this to DbEdit as it tries to apply patch and fails
let mut lock = data.conflicts.lock().unwrap(); let article = self.object.object.dereference(data).await?;
let conflict = DbConflict { let form = DbConflictForm {
id: random(), id: EditVersion::new(&self.object.content)?,
diff: self.object.content, diff: self.object.content,
article_id: self.object.object, article_id: article.id,
previous_version: self.object.previous_version, previous_version_id: self.object.previous_version,
}; };
lock.push(conflict); DbConflict::create(&form, &data.db_connection)?;
Ok(()) Ok(())
} }
} }

View file

@ -1,6 +1,6 @@
use crate::database::article::DbArticleForm; use crate::database::article::DbArticleForm;
use crate::database::edit::EditVersion;
use crate::database::instance::DbInstance; use crate::database::instance::DbInstance;
use crate::database::version::EditVersion;
use crate::database::{article::DbArticle, MyDataHandle}; use crate::database::{article::DbArticle, MyDataHandle};
use crate::error::Error; use crate::error::Error;
use crate::federation::objects::edits_collection::DbEditCollection; use crate::federation::objects::edits_collection::DbEditCollection;

View file

@ -1,5 +1,6 @@
use crate::database::article::DbArticle; use crate::database::article::DbArticle;
use crate::database::edit::{DbEdit, DbEditForm, EditVersion}; use crate::database::edit::{DbEdit, DbEditForm};
use crate::database::version::EditVersion;
use crate::database::MyDataHandle; use crate::database::MyDataHandle;
use crate::error::Error; use crate::error::Error;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
@ -44,9 +45,8 @@ impl Object for DbEdit {
kind: EditType::Edit, kind: EditType::Edit,
id: self.ap_id, id: self.ap_id,
content: self.diff, content: self.diff,
version: self.version, version: self.hash,
// TODO: this is wrong previous_version: self.previous_version_id,
previous_version: self.previous_version,
object: article.ap_id, object: article.ap_id,
}) })
} }
@ -65,8 +65,8 @@ impl Object for DbEdit {
ap_id: json.id, ap_id: json.id,
diff: json.content, diff: json.content,
article_id: article.id, article_id: article.id,
version: json.version, hash: json.version,
previous_version: json.previous_version, previous_version_id: json.previous_version,
}; };
let edit = DbEdit::create(&form, &data.db_connection)?; let edit = DbEdit::create(&form, &data.db_connection)?;
Ok(edit) Ok(edit)

View file

@ -1,6 +1,6 @@
use crate::api::api_routes; use crate::api::api_routes;
use crate::database::instance::{DbInstance, DbInstanceForm}; use crate::database::instance::{DbInstance, DbInstanceForm};
use crate::database::{FakeDatabase, MyData}; use crate::database::MyData;
use crate::error::MyResult; use crate::error::MyResult;
use crate::federation::routes::federation_routes; use crate::federation::routes::federation_routes;
use crate::utils::generate_activity_id; use crate::utils::generate_activity_id;
@ -28,10 +28,6 @@ mod utils;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> { pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
let fake_db = Arc::new(FakeDatabase {
conflicts: Mutex::new(vec![]),
});
let db_connection = Arc::new(Mutex::new(PgConnection::establish(database_url)?)); let db_connection = Arc::new(Mutex::new(PgConnection::establish(database_url)?));
db_connection db_connection
.lock() .lock()
@ -39,10 +35,7 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
.run_pending_migrations(MIGRATIONS) .run_pending_migrations(MIGRATIONS)
.unwrap(); .unwrap();
let data = MyData { let data = MyData { db_connection };
db_connection,
fake_db,
};
let config = FederationConfig::builder() let config = FederationConfig::builder()
.domain(hostname) .domain(hostname)
.app_data(data) .app_data(data)

View file

@ -1,5 +1,5 @@
use crate::database::edit::DbEdit; use crate::database::edit::DbEdit;
use crate::database::edit::EditVersion; use crate::database::version::EditVersion;
use crate::error::MyResult; use crate::error::MyResult;
use anyhow::anyhow; use anyhow::anyhow;
use diffy::{apply, Patch}; use diffy::{apply, Patch};
@ -31,7 +31,7 @@ pub fn generate_article_version(edits: &Vec<DbEdit>, version: &EditVersion) -> M
for e in edits { for e in edits {
let patch = Patch::from_str(&e.diff)?; let patch = Patch::from_str(&e.diff)?;
generated = apply(&generated, &patch)?; generated = apply(&generated, &patch)?;
if &e.version == version { if &e.hash == version {
return Ok(generated); return Ok(generated);
} }
} }

View file

@ -1,12 +1,14 @@
use anyhow::anyhow;
use fediwiki::api::{ use fediwiki::api::{
ApiConflict, CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject, CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
}; };
use fediwiki::database::article::ArticleView; use fediwiki::database::article::ArticleView;
use fediwiki::database::conflict::ApiConflict;
use fediwiki::database::instance::DbInstance; use fediwiki::database::instance::DbInstance;
use fediwiki::error::MyResult; use fediwiki::error::MyResult;
use fediwiki::start; use fediwiki::start;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use reqwest::Client; use reqwest::{Client, RequestBuilder, StatusCode};
use serde::de::Deserialize; use serde::de::Deserialize;
use serde::ser::Serialize; use serde::ser::Serialize;
use std::env::current_dir; use std::env::current_dir;
@ -142,7 +144,7 @@ pub async fn create_article(hostname: &str, title: String) -> MyResult<ArticleVi
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: article.article.id, article_id: article.article.id,
new_text: TEST_ARTICLE_DEFAULT_TEXT.to_string(), new_text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
previous_version: article.latest_version, previous_version_id: article.latest_version,
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
edit_article(hostname, &edit_form).await edit_article(hostname, &edit_form).await
@ -157,13 +159,10 @@ pub async fn edit_article_with_conflict(
hostname: &str, hostname: &str,
edit_form: &EditArticleData, edit_form: &EditArticleData,
) -> MyResult<Option<ApiConflict>> { ) -> MyResult<Option<ApiConflict>> {
Ok(CLIENT let req = CLIENT
.patch(format!("http://{}/api/v1/article", hostname)) .patch(format!("http://{}/api/v1/article", hostname))
.form(edit_form) .form(edit_form);
.send() handle_json_res(req).await
.await?
.json()
.await?)
} }
pub async fn edit_article(hostname: &str, edit_form: &EditArticleData) -> MyResult<ArticleView> { pub async fn edit_article(hostname: &str, edit_form: &EditArticleData) -> MyResult<ArticleView> {
@ -184,25 +183,34 @@ where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
R: Serialize, R: Serialize,
{ {
let mut res = CLIENT.get(format!("http://{}/api/v1/{}", hostname, endpoint)); let mut req = CLIENT.get(format!("http://{}/api/v1/{}", hostname, endpoint));
if let Some(query) = query { if let Some(query) = query {
res = res.query(&query); req = req.query(&query);
} }
let alpha_instance: T = res.send().await?.json().await?; handle_json_res(req).await
Ok(alpha_instance)
} }
pub async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R> pub async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
where where
R: for<'de> Deserialize<'de>, R: for<'de> Deserialize<'de>,
{ {
Ok(CLIENT let req = CLIENT
.post(format!("http://{}/api/v1/{}", hostname, endpoint)) .post(format!("http://{}/api/v1/{}", hostname, endpoint))
.form(form) .form(form);
.send() handle_json_res(req).await
.await? }
.json()
.await?) async fn handle_json_res<T>(req: RequestBuilder) -> MyResult<T>
where
T: for<'de> Deserialize<'de>,
{
let res = req.send().await?;
if res.status() == StatusCode::OK {
Ok(res.json().await?)
} else {
let text = res.text().await?;
Err(anyhow!("Post API response {text}").into())
}
} }
pub async fn follow_instance(api_instance: &str, follow_instance: &str) -> MyResult<()> { pub async fn follow_instance(api_instance: &str, follow_instance: &str) -> MyResult<()> {

View file

@ -7,12 +7,11 @@ use crate::common::{
get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT, get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT,
}; };
use common::get; use common::get;
use fediwiki::api::{ use fediwiki::api::{EditArticleData, ForkArticleData, ResolveObject, SearchArticleData};
ApiConflict, EditArticleData, ForkArticleData, ResolveObject, SearchArticleData,
};
use fediwiki::database::article::{ArticleView, DbArticle}; use fediwiki::database::article::{ArticleView, DbArticle};
use fediwiki::error::MyResult; use fediwiki::error::MyResult;
use fediwiki::database::conflict::ApiConflict;
use fediwiki::database::instance::{DbInstance, InstanceView}; use fediwiki::database::instance::{DbInstance, InstanceView};
use pretty_assertions::{assert_eq, assert_ne}; use pretty_assertions::{assert_eq, assert_ne};
use url::Url; use url::Url;
@ -42,7 +41,7 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum 2".to_string(), new_text: "Lorem Ipsum 2".to_string(),
previous_version: get_res.latest_version, previous_version_id: get_res.latest_version,
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?; let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
@ -125,7 +124,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum 2\n".to_string(), new_text: "Lorem Ipsum 2\n".to_string(),
previous_version: create_res.latest_version, previous_version_id: create_res.latest_version,
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
edit_article(&data.alpha.hostname, &edit_form).await?; edit_article(&data.alpha.hostname, &edit_form).await?;
@ -179,7 +178,7 @@ async fn test_edit_local_article() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum 2".to_string(), new_text: "Lorem Ipsum 2".to_string(),
previous_version: get_res.latest_version, previous_version_id: get_res.latest_version,
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article(&data.beta.hostname, &edit_form).await?; let edit_res = edit_article(&data.beta.hostname, &edit_form).await?;
@ -225,7 +224,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum 2".to_string(), new_text: "Lorem Ipsum 2".to_string(),
previous_version: get_res.latest_version, previous_version_id: get_res.latest_version,
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?; let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
@ -265,7 +264,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum\n".to_string(), new_text: "Lorem Ipsum\n".to_string(),
previous_version: create_res.latest_version.clone(), previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?; let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
@ -276,7 +275,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Ipsum Lorem\n".to_string(), new_text: "Ipsum Lorem\n".to_string(),
previous_version: create_res.latest_version, previous_version_id: create_res.latest_version,
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article_with_conflict(&data.alpha.hostname, &edit_form) let edit_res = edit_article_with_conflict(&data.alpha.hostname, &edit_form)
@ -292,7 +291,7 @@ async fn test_local_edit_conflict() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum and Ipsum Lorem\n".to_string(), new_text: "Lorem Ipsum and Ipsum Lorem\n".to_string(),
previous_version: edit_res.previous_version, previous_version_id: edit_res.previous_version_id,
resolve_conflict_id: Some(edit_res.id), resolve_conflict_id: Some(edit_res.id),
}; };
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?; let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
@ -333,7 +332,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "Lorem Ipsum\n".to_string(), new_text: "Lorem Ipsum\n".to_string(),
previous_version: create_res.latest_version.clone(), previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?; let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
@ -350,7 +349,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "aaaa\n".to_string(), new_text: "aaaa\n".to_string(),
previous_version: create_res.latest_version, previous_version_id: create_res.latest_version,
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?; let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
@ -367,8 +366,8 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "aaaa\n".to_string(), new_text: "aaaa\n".to_string(),
previous_version: conflicts[0].previous_version.clone(), previous_version_id: conflicts[0].previous_version_id.clone(),
resolve_conflict_id: Some(conflicts[0].id), resolve_conflict_id: Some(conflicts[0].id.clone()),
}; };
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?; let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
assert_eq!(edit_form.new_text, edit_res.article.text); assert_eq!(edit_form.new_text, edit_res.article.text);
@ -395,7 +394,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "my\nexample\ntext\n".to_string(), new_text: "my\nexample\ntext\n".to_string(),
previous_version: create_res.latest_version.clone(), previous_version_id: create_res.latest_version.clone(),
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?; let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
@ -406,7 +405,7 @@ async fn test_overlapping_edits_no_conflict() -> MyResult<()> {
let edit_form = EditArticleData { let edit_form = EditArticleData {
article_id: create_res.article.id, article_id: create_res.article.id,
new_text: "some\nexample\narticle\n".to_string(), new_text: "some\nexample\narticle\n".to_string(),
previous_version: create_res.latest_version, previous_version_id: create_res.latest_version,
resolve_conflict_id: None, resolve_conflict_id: None,
}; };
let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?; let edit_res = edit_article(&data.alpha.hostname, &edit_form).await?;
@ -448,7 +447,7 @@ async fn test_fork_article() -> MyResult<()> {
assert_eq!(resolved_article.text, forked_article.text); assert_eq!(resolved_article.text, forked_article.text);
assert_eq!(resolve_res.edits.len(), fork_res.edits.len()); assert_eq!(resolve_res.edits.len(), fork_res.edits.len());
assert_eq!(resolve_res.edits[0].diff, fork_res.edits[0].diff); assert_eq!(resolve_res.edits[0].diff, fork_res.edits[0].diff);
assert_eq!(resolve_res.edits[0].version, fork_res.edits[0].version); assert_eq!(resolve_res.edits[0].hash, fork_res.edits[0].hash);
assert_ne!(resolve_res.edits[0].id, fork_res.edits[0].id); assert_ne!(resolve_res.edits[0].id, fork_res.edits[0].id);
assert_eq!(resolve_res.latest_version, fork_res.latest_version); assert_eq!(resolve_res.latest_version, fork_res.latest_version);
assert_ne!(resolved_article.ap_id, forked_article.ap_id); assert_ne!(resolved_article.ap_id, forked_article.ap_id);