Skip to content

Commit

Permalink
exposing Automerge::Diff through to Swift (#183)
Browse files Browse the repository at this point in the history
* bindings: difference between document change-hash
* api: patches between two set of change hash
* api: patches from/to commits in the doc history
* uni tests: differences between points in the history
* missing method into curation doc
* minor: typo in the difference binding api description
* Update Sources/Automerge/Document.swift
Co-authored-by: Joseph Heck <heckj@mac.com>
* comments in PR #183

---------

Co-authored-by: Joseph Heck <heckj@mac.com>
  • Loading branch information
miguelangel-dev and heckj authored Jun 7, 2024
1 parent 830dd77 commit 17238f5
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Sources/Automerge/Automerge.docc/Curation/Document.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
- ``heads()``
- ``getHistory()``
- ``change(hash:)``
- ``difference(from:to:)``
- ``difference(since:)``
- ``difference(to:)``

### Reading historical map values

Expand Down
60 changes: 60 additions & 0 deletions Sources/Automerge/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,66 @@ public final class Document: @unchecked Sendable {
}
}

/// Generates patches between two points in the document history.
///
/// Use:
/// ```
/// let doc = Document()
/// let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
/// let before = doc.heads()
///
/// try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello")
/// let after = doc.heads()
///
/// let patches = doc.difference(from: before, to: after)
/// ```
///
/// - Parameters:
/// - from: The set of heads at beginning point in the documents history.
/// - to: The set of heads at ending point in the documents history.
/// - Note: `from` and `to` do not have to be chronological. Document state can move backward.
/// - Returns: The difference needed to produce a document at `to` when it is set at `from` in history.
public func difference(from before: Set<ChangeHash>, to after: Set<ChangeHash>) -> [Patch] {
sync {
let patches = self.doc.wrapErrors { doc in
doc.difference(before: before.map(\.bytes), after: after.map(\.bytes))
}
return patches.map { Patch($0) }
}
}

/// Generates patches **since** a given point in the document history.
///
/// Use:
/// ```
/// let doc = Document()
/// doc.difference(since: doc.heads())
/// ```
///
/// - Parameters:
/// - since: The set of heads at the point in the documents history to compare to.
/// - Returns: The difference needed to produce current document given an arbitrary
/// point in the history.
public func difference(since lhs: Set<ChangeHash>) -> [Patch] {
difference(from: lhs, to: heads())
}

/// Generates patches **to** a given point in the document history.
///
/// Use:
/// ```
/// let doc = Document()
/// doc.difference(to: doc.heads())
/// ```
///
/// - Parameters:
/// - to: The set of heads at ending point in the documents history.
/// - Returns: The difference needed to move current document to a previous point
/// in the history.
public func difference(to rhs: Set<ChangeHash>) -> [Patch] {
difference(from: heads(), to: rhs)
}

/// Get the path to an object within the document.
///
/// - Parameter obj: The identifier of an array, dictionary or text object.
Expand Down
53 changes: 53 additions & 0 deletions Tests/AutomergeTests/TestChanges.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,57 @@ class ChangeSetTests: XCTestCase {
XCTAssertEqual(bytes, data)
// print(data.hexEncodedString())
}

func testDifferenceToPreviousCommit() throws {
let doc = Document()
let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello")

let before = doc.heads()
try doc.spliceText(obj: textId, start: 5, delete: 0, value: " World 👨‍👩‍👧‍👦")

let patches = doc.difference(to: before)
let length = UInt64(" World 👨‍👩‍👧‍👦".unicodeScalars.count)
XCTAssertEqual(patches.count, 1)
XCTAssertEqual(patches.first?.action, .DeleteSeq(DeleteSeq(obj: textId, index: 5, length: length)))
}

func testDifferenceSincePreviousCommit() throws {
let doc = Document()
let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello")

let before = doc.heads()
try doc.spliceText(obj: textId, start: 5, delete: 0, value: " World 👨‍👩‍👧‍👦")

let patches = doc.difference(since: before)
XCTAssertEqual(patches.count, 1)
XCTAssertEqual(patches.first?.action, .SpliceText(obj: textId, index: 5, value: " World 👨‍👩‍👧‍👦", marks: [:]))
}

func testDifferenceBetweenTwoCommitsInHistory() throws {
let doc = Document()
let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
let before = doc.heads()
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello")
try doc.spliceText(obj: textId, start: 5, delete: 0, value: " World 👨‍👩‍👧‍👦")
let after = doc.heads()

let patches = doc.difference(from: before, to: after)
XCTAssertEqual(patches.count, 1)
XCTAssertEqual(patches.first?.action, .SpliceText(obj: textId, index: 0, value: "Hello World 👨‍👩‍👧‍👦", marks: [:]))
}

func testDifferenceProperty_DifferenceBetweenCommitAndCurrent_DifferenceSinceCommit_ResultsEquals() throws {
let doc = Document()
let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
let before = doc.heads()
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello")
try doc.spliceText(obj: textId, start: 5, delete: 0, value: " World 👨‍👩‍👧‍👦")

let patches1 = doc.difference(from: before, to: doc.heads())
let patches2 = doc.difference(since: before)
XCTAssertEqual(patches1.count, 1)
XCTAssertEqual(patches1, patches2)
}
}
2 changes: 2 additions & 0 deletions rust/src/automerge.udl
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ interface Doc {

Change? change_by_hash(ChangeHash hash);

sequence<Patch> difference(sequence<ChangeHash> before, sequence<ChangeHash> after);

void commit_with(string? msg, i64 time);

sequence<u8> save();
Expand Down
14 changes: 14 additions & 0 deletions rust/src/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,20 @@ impl Doc {
changes.into_iter().map(|h| h.hash().into()).collect()
}

pub fn difference(&self, before: Vec<ChangeHash>, after: Vec<ChangeHash>) -> Vec<Patch> {
let lhs = before
.into_iter()
.map(am::ChangeHash::from)
.collect::<Vec<_>>();
let rhs = after
.into_iter()
.map(am::ChangeHash::from)
.collect::<Vec<_>>();
let mut doc = self.0.write().unwrap();
let patches = doc.diff(&lhs, &rhs);
patches.into_iter().map(Patch::from).collect()
}

pub fn change_by_hash(&self, hash: ChangeHash) -> Option<Change> {
let doc = self.0.write().unwrap();
doc.get_change_by_hash(&am::ChangeHash::from(hash))
Expand Down

0 comments on commit 17238f5

Please sign in to comment.