mirror of
https://github.com/Nutomic/ibis.git
synced 2024-12-23 09:01:22 +00:00
conflict moved to db
This commit is contained in:
parent
37352a3e86
commit
5d2099c17c
19 changed files with 342 additions and 254 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -492,6 +492,7 @@ dependencies = [
|
|||
"diesel_derives",
|
||||
"itoa",
|
||||
"pq-sys",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -679,6 +680,7 @@ dependencies = [
|
|||
"enum_delegate",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"hex",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
"rand",
|
||||
|
@ -688,6 +690,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -924,6 +927,12 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.11"
|
||||
|
@ -2276,11 +2285,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.5.0"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -10,19 +10,21 @@ async-trait = "0.1.74"
|
|||
axum = "0.6.20"
|
||||
axum-macros = "0.3.8"
|
||||
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_migrations = "2.1.0"
|
||||
diffy = "0.3.0"
|
||||
enum_delegate = "0.2.0"
|
||||
env_logger = { version = "0.10.1", default-features = false }
|
||||
futures = "0.3.29"
|
||||
hex = "0.4.3"
|
||||
rand = "0.8.5"
|
||||
serde = "1.0.192"
|
||||
sha2 = "0.10.8"
|
||||
tokio = { version = "1.34.0", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
url = "2.4.1"
|
||||
uuid = { version = "1.6.1", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
once_cell = "1.18.0"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
drop table conflict;
|
||||
drop table edit;
|
||||
drop table article;
|
||||
drop table instance_follow;
|
||||
drop table instance;
|
||||
drop table instance;
|
||||
|
|
|
@ -28,9 +28,16 @@ create table article (
|
|||
|
||||
create table edit (
|
||||
id serial primary key,
|
||||
hash uuid not null,
|
||||
ap_id varchar(255) not null unique,
|
||||
diff text not null,
|
||||
article_id int REFERENCES article ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
|
||||
version text not null,
|
||||
previous_version text not null
|
||||
)
|
||||
previous_version_id uuid 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
|
||||
);
|
73
src/api.rs
73
src/api.rs
|
@ -1,7 +1,9 @@
|
|||
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::{DbConflict, MyDataHandle};
|
||||
use crate::database::version::EditVersion;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::activities::create_article::CreateArticle;
|
||||
use crate::federation::activities::follow::Follow;
|
||||
|
@ -9,14 +11,12 @@ 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 anyhow::anyhow;
|
||||
use axum::extract::Query;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Form, Json, Router};
|
||||
use axum_macros::debug_handler;
|
||||
use diffy::create_patch;
|
||||
use futures::future::try_join_all;
|
||||
use rand::random;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
|
@ -60,8 +60,7 @@ async fn create_article(
|
|||
instance_id: local_instance.id,
|
||||
local: true,
|
||||
};
|
||||
dbg!(&form.ap_id);
|
||||
let article = dbg!(DbArticle::create(&form, &data.db_connection))?;
|
||||
let article = DbArticle::create(&form, &data.db_connection)?;
|
||||
|
||||
CreateArticle::send_to_followers(article.clone(), &data).await?;
|
||||
|
||||
|
@ -77,17 +76,9 @@ pub struct EditArticleData {
|
|||
pub new_text: String,
|
||||
/// The version that this edit is based on, ie [DbArticle.latest_version] or
|
||||
/// [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
|
||||
pub resolve_conflict_id: Option<i32>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
pub resolve_conflict_id: Option<EditVersion>,
|
||||
}
|
||||
|
||||
/// Edit an existing article (local or remote).
|
||||
|
@ -105,21 +96,17 @@ async fn edit_article(
|
|||
Form(edit_form): Form<EditArticleData>,
|
||||
) -> MyResult<Json<Option<ApiConflict>>> {
|
||||
// resolve conflict if any
|
||||
if let Some(resolve_conflict_id) = &edit_form.resolve_conflict_id {
|
||||
let mut lock = data.conflicts.lock().unwrap();
|
||||
if !lock.iter().any(|c| &c.id == resolve_conflict_id) {
|
||||
return Err(anyhow!("invalid resolve conflict"))?;
|
||||
}
|
||||
lock.retain(|c| &c.id != resolve_conflict_id);
|
||||
if let Some(resolve_conflict_id) = edit_form.resolve_conflict_id {
|
||||
DbConflict::delete(resolve_conflict_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
|
||||
submit_article_update(
|
||||
&data,
|
||||
edit_form.new_text.clone(),
|
||||
edit_form.previous_version,
|
||||
edit_form.previous_version_id,
|
||||
&original_article.article,
|
||||
)
|
||||
.await?;
|
||||
|
@ -128,20 +115,18 @@ async fn edit_article(
|
|||
// 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.
|
||||
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 db_conflict = DbConflict {
|
||||
id: random(),
|
||||
let previous_version = DbEdit::read(&edit_form.previous_version_id, &data.db_connection)?;
|
||||
let form = DbConflictForm {
|
||||
id: EditVersion::new(&patch.to_string())?,
|
||||
diff: patch.to_string(),
|
||||
article_id: original_article.article.ap_id.clone(),
|
||||
previous_version: edit_form.previous_version,
|
||||
article_id: original_article.article.id,
|
||||
previous_version_id: previous_version.hash,
|
||||
};
|
||||
{
|
||||
let mut lock = data.conflicts.lock().unwrap();
|
||||
lock.push(db_conflict.clone());
|
||||
}
|
||||
Ok(Json(db_conflict.to_api_conflict(&data).await?))
|
||||
let conflict = DbConflict::create(&form, &data.db_connection)?;
|
||||
Ok(Json(conflict.to_api_conflict(&data).await?))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,8 +171,8 @@ async fn resolve_article(
|
|||
data: Data<MyDataHandle>,
|
||||
) -> MyResult<Json<ArticleView>> {
|
||||
let article: DbArticle = ObjectId::from(query.id).dereference(&data).await?;
|
||||
let edits = DbEdit::for_article(&article, &data.db_connection)?;
|
||||
let latest_version = edits.last().unwrap().version.clone();
|
||||
let edits = DbEdit::read_for_article(&article, &data.db_connection)?;
|
||||
let latest_version = edits.last().unwrap().hash.clone();
|
||||
Ok(Json(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
|
@ -226,7 +211,7 @@ async fn follow_instance(
|
|||
/// Get a list of all unresolved edit conflicts.
|
||||
#[debug_handler]
|
||||
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 data = data.reset_request_count();
|
||||
async move { c.to_api_conflict(&data).await }
|
||||
|
@ -289,10 +274,18 @@ async fn fork_article(
|
|||
|
||||
// copy edits to new article
|
||||
// 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 {
|
||||
let form = e.copy_to_local_fork(&article)?;
|
||||
DbEdit::create(&form, &data.db_connection)?;
|
||||
let ap_id = DbEditForm::generate_ap_id(&article, &e.hash)?;
|
||||
// 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?;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::database::edit::{DbEdit, EditVersion};
|
||||
use crate::database::edit::DbEdit;
|
||||
|
||||
use crate::database::schema::article;
|
||||
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::object_id::ObjectId;
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::BelongingToDsl;
|
||||
|
||||
use diesel::ExpressionMethods;
|
||||
use diesel::{
|
||||
insert_into, AsChangeset, BoolExpressionMethods, Identifiable, Insertable,
|
||||
|
@ -14,6 +14,7 @@ use diesel::{
|
|||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::version::EditVersion;
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Mutex;
|
||||
|
||||
|
@ -86,8 +87,7 @@ impl DbArticle {
|
|||
article::table.find(id).get_result(conn.deref_mut())?
|
||||
};
|
||||
let latest_version = article.latest_edit_version(conn)?;
|
||||
let mut conn = conn.lock().unwrap();
|
||||
let edits: Vec<DbEdit> = DbEdit::belonging_to(&article).get_results(conn.deref_mut())?;
|
||||
let edits: Vec<DbEdit> = DbEdit::read_for_article(&article, conn)?;
|
||||
Ok(ArticleView {
|
||||
article,
|
||||
edits,
|
||||
|
@ -137,9 +137,8 @@ impl DbArticle {
|
|||
|
||||
// TODO: shouldnt have to read all edits from db
|
||||
pub fn latest_edit_version(&self, conn: &Mutex<PgConnection>) -> MyResult<EditVersion> {
|
||||
let mut conn = conn.lock().unwrap();
|
||||
let edits: Vec<DbEdit> = DbEdit::belonging_to(&self).get_results(conn.deref_mut())?;
|
||||
match edits.last().map(|e| e.version.clone()) {
|
||||
let edits: Vec<DbEdit> = DbEdit::read_for_article(self, conn)?;
|
||||
match edits.last().map(|e| e.hash.clone()) {
|
||||
Some(latest_version) => Ok(latest_version),
|
||||
None => Ok(EditVersion::default()),
|
||||
}
|
||||
|
|
108
src/database/conflict.rs
Normal file
108
src/database/conflict.rs
Normal 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)?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,80 +1,71 @@
|
|||
use crate::database::schema::edit;
|
||||
use crate::database::version::EditVersion;
|
||||
use crate::database::DbArticle;
|
||||
use crate::error::MyResult;
|
||||
use activitypub_federation::fetch::object_id::ObjectId;
|
||||
use diesel::ExpressionMethods;
|
||||
use diesel::{
|
||||
insert_into, AsChangeset, Identifiable, Insertable, PgConnection, Queryable, RunQueryDsl,
|
||||
insert_into, AsChangeset, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl,
|
||||
Selectable,
|
||||
};
|
||||
use diesel::{Associations, BelongingToDsl};
|
||||
use diesel_derive_newtype::DieselNewType;
|
||||
use diffy::create_patch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha224};
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Represents a single change to the article.
|
||||
#[derive(
|
||||
Clone,
|
||||
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))]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable)]
|
||||
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
|
||||
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,
|
||||
/// UUID built from sha224 hash of diff
|
||||
pub hash: EditVersion,
|
||||
pub ap_id: ObjectId<DbEdit>,
|
||||
pub diff: String,
|
||||
pub article_id: i32,
|
||||
pub version: EditVersion,
|
||||
// TODO: could be an Option<DbEdit.id> instead
|
||||
pub previous_version: EditVersion,
|
||||
/// First edit of an article always has `EditVersion::default()` here
|
||||
pub previous_version_id: EditVersion,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
||||
#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))]
|
||||
pub struct DbEditForm {
|
||||
pub hash: EditVersion,
|
||||
pub ap_id: ObjectId<DbEdit>,
|
||||
pub diff: String,
|
||||
pub article_id: i32,
|
||||
pub version: EditVersion,
|
||||
pub previous_version: EditVersion,
|
||||
pub previous_version_id: EditVersion,
|
||||
}
|
||||
|
||||
impl DbEditForm {
|
||||
pub fn new(
|
||||
original_article: &DbArticle,
|
||||
updated_text: &str,
|
||||
previous_version: EditVersion,
|
||||
previous_version_id: EditVersion,
|
||||
) -> MyResult<Self> {
|
||||
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 {
|
||||
hash: version,
|
||||
ap_id,
|
||||
diff: diff.to_string(),
|
||||
article_id: original_article.id,
|
||||
version: EditVersion(hash),
|
||||
previous_version,
|
||||
previous_version_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_ap_id_and_hash(
|
||||
pub(crate) fn generate_ap_id(
|
||||
article: &DbArticle,
|
||||
diff: Vec<u8>,
|
||||
) -> MyResult<(ObjectId<DbEdit>, String)> {
|
||||
let mut sha224 = Sha224::new();
|
||||
sha224.update(diff);
|
||||
let hash = format!("{:X}", sha224.finalize());
|
||||
Ok((
|
||||
ObjectId::parse(&format!("{}/{}", article.ap_id, hash))?,
|
||||
hash,
|
||||
))
|
||||
version: &EditVersion,
|
||||
) -> MyResult<ObjectId<DbEdit>> {
|
||||
Ok(ObjectId::parse(&format!(
|
||||
"{}/{}",
|
||||
article.ap_id,
|
||||
version.hash()
|
||||
))?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,31 +79,20 @@ impl DbEdit {
|
|||
.set(form)
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
|
||||
pub fn for_article(article: &DbArticle, conn: &Mutex<PgConnection>) -> MyResult<Vec<Self>> {
|
||||
pub fn read(version: &EditVersion, conn: &Mutex<PgConnection>) -> MyResult<Self> {
|
||||
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))
|
||||
.get_result(conn.deref_mut())?)
|
||||
}
|
||||
pub fn copy_to_local_fork(self, article: &DbArticle) -> MyResult<DbEditForm> {
|
||||
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 struct EditVersion(pub String);
|
||||
|
||||
impl Default for EditVersion {
|
||||
fn default() -> Self {
|
||||
let sha224 = Sha224::new();
|
||||
let hash = format!("{:X}", sha224.finalize());
|
||||
EditVersion(hash)
|
||||
|
||||
pub fn read_for_article(
|
||||
article: &DbArticle,
|
||||
conn: &Mutex<PgConnection>,
|
||||
) -> MyResult<Vec<Self>> {
|
||||
let mut conn = conn.lock().unwrap();
|
||||
Ok(edit::table
|
||||
.filter(edit::dsl::article_id.eq(article.id))
|
||||
.get_results(conn.deref_mut())?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,90 +1,27 @@
|
|||
use crate::api::ApiConflict;
|
||||
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::sync::{Arc, Mutex};
|
||||
|
||||
pub mod article;
|
||||
pub mod conflict;
|
||||
pub mod edit;
|
||||
pub mod instance;
|
||||
mod schema;
|
||||
pub mod version;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MyData {
|
||||
pub db_connection: Arc<Mutex<PgConnection>>,
|
||||
pub fake_db: Arc<FakeDatabase>,
|
||||
}
|
||||
|
||||
impl Deref for MyData {
|
||||
type Target = Arc<FakeDatabase>;
|
||||
type Target = Arc<Mutex<PgConnection>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.fake_db
|
||||
&self.db_connection
|
||||
}
|
||||
}
|
||||
|
||||
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)?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,15 +12,24 @@ diesel::table! {
|
|||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
conflict (id) {
|
||||
id -> Uuid,
|
||||
diff -> Text,
|
||||
article_id -> Int4,
|
||||
previous_version_id -> Uuid,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
edit (id) {
|
||||
id -> Int4,
|
||||
hash -> Uuid,
|
||||
#[max_length = 255]
|
||||
ap_id -> Varchar,
|
||||
diff -> Text,
|
||||
article_id -> Int4,
|
||||
version -> Text,
|
||||
previous_version -> Text,
|
||||
previous_version_id -> Uuid,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,6 +58,7 @@ diesel::table! {
|
|||
}
|
||||
|
||||
diesel::joinable!(article -> instance (instance_id));
|
||||
diesel::joinable!(conflict -> 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
43
src/database/version.rs
Normal 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(())
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
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::version::EditVersion;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::Error;
|
||||
use crate::federation::activities::update_local_article::UpdateLocalArticle;
|
||||
|
@ -30,12 +31,12 @@ pub async fn submit_article_update(
|
|||
} else {
|
||||
// dont insert edit into db, might be invalid in case of conflict
|
||||
let edit = DbEdit {
|
||||
id: 0,
|
||||
id: -1,
|
||||
hash: form.hash,
|
||||
ap_id: form.ap_id,
|
||||
diff: form.diff,
|
||||
article_id: form.article_id,
|
||||
version: form.version,
|
||||
previous_version: form.previous_version,
|
||||
previous_version_id: form.previous_version_id,
|
||||
};
|
||||
let instance = DbInstance::read(original_article.instance_id, &data.db_connection)?;
|
||||
UpdateRemoteArticle::send(edit, instance, data).await?;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use crate::database::conflict::{DbConflict, DbConflictForm};
|
||||
use crate::database::instance::DbInstance;
|
||||
use crate::database::version::EditVersion;
|
||||
use crate::database::MyDataHandle;
|
||||
use crate::error::MyResult;
|
||||
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,
|
||||
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 url::Url;
|
||||
|
||||
|
@ -68,16 +66,15 @@ impl ActivityHandler for RejectEdit {
|
|||
}
|
||||
|
||||
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
|
||||
let mut lock = data.conflicts.lock().unwrap();
|
||||
let conflict = DbConflict {
|
||||
id: random(),
|
||||
let article = self.object.object.dereference(data).await?;
|
||||
let form = DbConflictForm {
|
||||
id: EditVersion::new(&self.object.content)?,
|
||||
diff: self.object.content,
|
||||
article_id: self.object.object,
|
||||
previous_version: self.object.previous_version,
|
||||
article_id: article.id,
|
||||
previous_version_id: self.object.previous_version,
|
||||
};
|
||||
lock.push(conflict);
|
||||
DbConflict::create(&form, &data.db_connection)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::database::article::DbArticleForm;
|
||||
use crate::database::edit::EditVersion;
|
||||
use crate::database::instance::DbInstance;
|
||||
use crate::database::version::EditVersion;
|
||||
use crate::database::{article::DbArticle, MyDataHandle};
|
||||
use crate::error::Error;
|
||||
use crate::federation::objects::edits_collection::DbEditCollection;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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::error::Error;
|
||||
use activitypub_federation::config::Data;
|
||||
|
@ -44,9 +45,8 @@ impl Object for DbEdit {
|
|||
kind: EditType::Edit,
|
||||
id: self.ap_id,
|
||||
content: self.diff,
|
||||
version: self.version,
|
||||
// TODO: this is wrong
|
||||
previous_version: self.previous_version,
|
||||
version: self.hash,
|
||||
previous_version: self.previous_version_id,
|
||||
object: article.ap_id,
|
||||
})
|
||||
}
|
||||
|
@ -65,8 +65,8 @@ impl Object for DbEdit {
|
|||
ap_id: json.id,
|
||||
diff: json.content,
|
||||
article_id: article.id,
|
||||
version: json.version,
|
||||
previous_version: json.previous_version,
|
||||
hash: json.version,
|
||||
previous_version_id: json.previous_version,
|
||||
};
|
||||
let edit = DbEdit::create(&form, &data.db_connection)?;
|
||||
Ok(edit)
|
||||
|
|
11
src/lib.rs
11
src/lib.rs
|
@ -1,6 +1,6 @@
|
|||
use crate::api::api_routes;
|
||||
use crate::database::instance::{DbInstance, DbInstanceForm};
|
||||
use crate::database::{FakeDatabase, MyData};
|
||||
use crate::database::MyData;
|
||||
use crate::error::MyResult;
|
||||
use crate::federation::routes::federation_routes;
|
||||
use crate::utils::generate_activity_id;
|
||||
|
@ -28,10 +28,6 @@ mod utils;
|
|||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
|
||||
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)?));
|
||||
db_connection
|
||||
.lock()
|
||||
|
@ -39,10 +35,7 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> {
|
|||
.run_pending_migrations(MIGRATIONS)
|
||||
.unwrap();
|
||||
|
||||
let data = MyData {
|
||||
db_connection,
|
||||
fake_db,
|
||||
};
|
||||
let data = MyData { db_connection };
|
||||
let config = FederationConfig::builder()
|
||||
.domain(hostname)
|
||||
.app_data(data)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::database::edit::DbEdit;
|
||||
use crate::database::edit::EditVersion;
|
||||
use crate::database::version::EditVersion;
|
||||
use crate::error::MyResult;
|
||||
use anyhow::anyhow;
|
||||
use diffy::{apply, Patch};
|
||||
|
@ -31,7 +31,7 @@ pub fn generate_article_version(edits: &Vec<DbEdit>, version: &EditVersion) -> M
|
|||
for e in edits {
|
||||
let patch = Patch::from_str(&e.diff)?;
|
||||
generated = apply(&generated, &patch)?;
|
||||
if &e.version == version {
|
||||
if &e.hash == version {
|
||||
return Ok(generated);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use anyhow::anyhow;
|
||||
use fediwiki::api::{
|
||||
ApiConflict, CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
|
||||
CreateArticleData, EditArticleData, FollowInstance, GetArticleData, ResolveObject,
|
||||
};
|
||||
use fediwiki::database::article::ArticleView;
|
||||
use fediwiki::database::conflict::ApiConflict;
|
||||
use fediwiki::database::instance::DbInstance;
|
||||
use fediwiki::error::MyResult;
|
||||
use fediwiki::start;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Client;
|
||||
use reqwest::{Client, RequestBuilder, StatusCode};
|
||||
use serde::de::Deserialize;
|
||||
use serde::ser::Serialize;
|
||||
use std::env::current_dir;
|
||||
|
@ -142,7 +144,7 @@ pub async fn create_article(hostname: &str, title: String) -> MyResult<ArticleVi
|
|||
let edit_form = EditArticleData {
|
||||
article_id: article.article.id,
|
||||
new_text: TEST_ARTICLE_DEFAULT_TEXT.to_string(),
|
||||
previous_version: article.latest_version,
|
||||
previous_version_id: article.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
edit_article(hostname, &edit_form).await
|
||||
|
@ -157,13 +159,10 @@ pub async fn edit_article_with_conflict(
|
|||
hostname: &str,
|
||||
edit_form: &EditArticleData,
|
||||
) -> MyResult<Option<ApiConflict>> {
|
||||
Ok(CLIENT
|
||||
let req = CLIENT
|
||||
.patch(format!("http://{}/api/v1/article", hostname))
|
||||
.form(edit_form)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?)
|
||||
.form(edit_form);
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn edit_article(hostname: &str, edit_form: &EditArticleData) -> MyResult<ArticleView> {
|
||||
|
@ -184,25 +183,34 @@ where
|
|||
T: for<'de> Deserialize<'de>,
|
||||
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 {
|
||||
res = res.query(&query);
|
||||
req = req.query(&query);
|
||||
}
|
||||
let alpha_instance: T = res.send().await?.json().await?;
|
||||
Ok(alpha_instance)
|
||||
handle_json_res(req).await
|
||||
}
|
||||
|
||||
pub async fn post<T: Serialize, R>(hostname: &str, endpoint: &str, form: &T) -> MyResult<R>
|
||||
where
|
||||
R: for<'de> Deserialize<'de>,
|
||||
{
|
||||
Ok(CLIENT
|
||||
let req = CLIENT
|
||||
.post(format!("http://{}/api/v1/{}", hostname, endpoint))
|
||||
.form(form)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?)
|
||||
.form(form);
|
||||
handle_json_res(req).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<()> {
|
||||
|
|
|
@ -7,12 +7,11 @@ use crate::common::{
|
|||
get_query, post, TestData, TEST_ARTICLE_DEFAULT_TEXT,
|
||||
};
|
||||
use common::get;
|
||||
use fediwiki::api::{
|
||||
ApiConflict, EditArticleData, ForkArticleData, ResolveObject, SearchArticleData,
|
||||
};
|
||||
use fediwiki::api::{EditArticleData, ForkArticleData, ResolveObject, SearchArticleData};
|
||||
use fediwiki::database::article::{ArticleView, DbArticle};
|
||||
use fediwiki::error::MyResult;
|
||||
|
||||
use fediwiki::database::conflict::ApiConflict;
|
||||
use fediwiki::database::instance::{DbInstance, InstanceView};
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use url::Url;
|
||||
|
@ -42,7 +41,7 @@ async fn test_create_read_and_edit_article() -> MyResult<()> {
|
|||
let edit_form = EditArticleData {
|
||||
article_id: create_res.article.id,
|
||||
new_text: "Lorem Ipsum 2".to_string(),
|
||||
previous_version: get_res.latest_version,
|
||||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
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,
|
||||
};
|
||||
edit_article(&data.alpha.hostname, &edit_form).await?;
|
||||
|
@ -179,7 +178,7 @@ async fn test_edit_local_article() -> MyResult<()> {
|
|||
let edit_form = EditArticleData {
|
||||
article_id: create_res.article.id,
|
||||
new_text: "Lorem Ipsum 2".to_string(),
|
||||
previous_version: get_res.latest_version,
|
||||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
new_text: "Lorem Ipsum 2".to_string(),
|
||||
previous_version: get_res.latest_version,
|
||||
previous_version_id: get_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
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,
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
new_text: "Ipsum Lorem\n".to_string(),
|
||||
previous_version: create_res.latest_version,
|
||||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
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),
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
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,
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
new_text: "aaaa\n".to_string(),
|
||||
previous_version: create_res.latest_version,
|
||||
previous_version_id: create_res.latest_version,
|
||||
resolve_conflict_id: None,
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
new_text: "aaaa\n".to_string(),
|
||||
previous_version: conflicts[0].previous_version.clone(),
|
||||
resolve_conflict_id: Some(conflicts[0].id),
|
||||
previous_version_id: conflicts[0].previous_version_id.clone(),
|
||||
resolve_conflict_id: Some(conflicts[0].id.clone()),
|
||||
};
|
||||
let edit_res = edit_article(&data.gamma.hostname, &edit_form).await?;
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
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,
|
||||
};
|
||||
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 {
|
||||
article_id: create_res.article.id,
|
||||
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,
|
||||
};
|
||||
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!(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].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_eq!(resolve_res.latest_version, fork_res.latest_version);
|
||||
assert_ne!(resolved_article.ap_id, forked_article.ap_id);
|
||||
|
|
Loading…
Reference in a new issue