5.4. Replication and conflict model

Let’s take the following example to illustrate replication and conflicthandling.

  • Alice has a document containing Bob’s business card;
  • She synchronizes it between her desktop PC and her laptop;
  • On the desktop PC, she updates Bob’s E-mail address;Without syncing again, she updates Bob’s mobile number on the laptop;
  • Then she replicates the two to each other again.
    So on the desktop the document has Bob’s new E-mail address and his old mobilenumber, and on the laptop it has his old E-mail address and his new mobilenumber.

The question is, what happens to these conflicting updated documents?

5.4.1. CouchDB replication

CouchDB works with JSON documents inside databases. Replication of databasestakes place over HTTP, and can be either a “pull” or a “push”, but isunidirectional. So the easiest way to perform a full sync is to do a “push”followed by a “pull” (or vice versa).

So, Alice creates v1 and sync it. She updates to v2a on one side and v2b on theother, and then replicates. What happens?

The answer is simple: both versions exist on both sides!

  1. DESKTOP LAPTOP
  2. +---------+
  3. | /db/bob | INITIAL
  4. | v1 | CREATION
  5. +---------+
  6.  
  7. +---------+ +---------+
  8. | /db/bob | -----------------> | /db/bob | PUSH
  9. | v1 | | v1 |
  10. +---------+ +---------+
  11.  
  12. +---------+ +---------+ INDEPENDENT
  13. | /db/bob | | /db/bob | LOCAL
  14. | v2a | | v2b | EDITS
  15. +---------+ +---------+
  16.  
  17. +---------+ +---------+
  18. | /db/bob | -----------------> | /db/bob | PUSH
  19. | v2a | | v2a |
  20. +---------+ | v2b |
  21. +---------+
  22.  
  23. +---------+ +---------+
  24. | /db/bob | <----------------- | /db/bob | PULL
  25. | v2a | | v2a |
  26. | v2b | | v2b |
  27. +---------+ +---------+

After all, this is not a file system, so there’s no restriction that only onedocument can exist with the name /db/bob. These are just “conflicting” revisionsunder the same name.

Because the changes are always replicated, the data is safe. Both machines haveidentical copies of both documents, so failure of a hard drive on either sidewon’t lose any of the changes.

Another thing to notice is that peers do not have to be configured or tracked.You can do regular replications to peers, or you can do one-off, ad-hoc pushesor pulls. After the replication has taken place, there is no record kept ofwhich peer any particular document or revision came from.

So the question now is: what happens when you try to read /db/bob? By default,CouchDB picks one arbitrary revision as the “winner”, using a deterministicalgorithm so that the same choice will be made on all peers. The same happenswith views: the deterministically-chosen winner is the only revision fed intoyour map function.

Let’s say that the winner is v2a. On the desktop, if Alice reads the documentshe’ll see v2a, which is what she saved there. But on the laptop, afterreplication, she’ll also see only v2a. It could look as if the changes she madethere have been lost - but of course they have not, they have just been hiddenaway as a conflicting revision. But eventually she’ll need these changes mergedinto Bob’s business card, otherwise they will effectively have been lost.

Any sensible business-card application will, at minimum, have to present theconflicting versions to Alice and allow her to create a new versionincorporating information from them all. Ideally it would merge the updatesitself.

5.4.2. Conflict avoidance

When working on a single node, CouchDB will avoid creating conflicting revisionsby returning a 409 Conflict error. This is because, when youPUT a new version of a document, you must give the _rev of the previousversion. If that _rev has already been superseded, the update is rejectedwith a 409 Conflict response.

So imagine two users on the same node are fetching Bob’s business card, updatingit concurrently, and writing it back:

  1. USER1 -----------> GET /db/bob
  2. <----------- {"_rev":"1-aaa", ...}
  3.  
  4. USER2 -----------> GET /db/bob
  5. <----------- {"_rev":"1-aaa", ...}
  6.  
  7. USER1 -----------> PUT /db/bob?rev=1-aaa
  8. <----------- {"_rev":"2-bbb", ...}
  9.  
  10. USER2 -----------> PUT /db/bob?rev=1-aaa
  11. <----------- 409 Conflict (not saved)

User2’s changes are rejected, so it’s up to the app to fetch /db/bob again,and either:

  • apply the same changes as were applied to the earlier revision, and submita new PUT
  • redisplay the document so the user has to edit it again
  • just overwrite it with the document being saved before (which is notadvisable, as user1’s changes will be silently lost)
    So when working in this mode, your application still has to be able to handlethese conflicts and have a suitable retry strategy, but these conflicts neverend up inside the database itself.

5.4.3. Revision tree

When you update a document in CouchDB, it keeps a list of the previousrevisions. In the case where conflicting updates are introduced, this historybranches into a tree, where the current conflicting revisions for this documentform the tips (leaf nodes) of this tree:

  1. ,--> r2a
  2. r1 --> r2b
  3. `--> r2c

Each branch can then extend its history - for example if you read revision r2band then PUT with ?rev=r2b then you will make a new revision along thatparticular branch.

  1. ,--> r2a -> r3a -> r4a
  2. r1 --> r2b -> r3b
  3. `--> r2c -> r3c

Here, (r4a, r3b, r3c) are the set of conflicting revisions. The way you resolvea conflict is to delete the leaf nodes along the other branches. So when youcombine (r4a+r3b+r3c) into a single merged document, you would replace r4a anddelete r3b and r3c.

  1. ,--> r2a -> r3a -> r4a -> r5a
  2. r1 --> r2b -> r3b -> (r4b deleted)
  3. `--> r2c -> r3c -> (r4c deleted)

Note that r4b and r4c still exist as leaf nodes in the history tree, but asdeleted docs. You can retrieve them but they will be marked "_deleted":true.

When you compact a database, the bodies of all the non-leaf documents arediscarded. However, the list of historical _revs is retained, for the benefit oflater conflict resolution in case you meet any old replicas of the database atsome time in future. There is “revision pruning” to stop this gettingarbitrarily large.

5.4.4. Working with conflicting documents

The basic :get:/{doc}/{docid} operation will not show you anyinformation about conflicts. You see only the deterministically-chosen winner,and get no indication as to whether other conflicting revisions exist or not:

  1. {
  2. "_id":"test",
  3. "_rev":"2-b91bb807b4685080c6a651115ff558f5",
  4. "hello":"bar"
  5. }

If you do GET /db/test?conflicts=true, and the document is in a conflictstate, then you will get the winner plus a _conflicts member containing an arrayof the revs of the other, conflicting revision(s). You can then fetch themindividually using subsequent GET /db/test?rev=xxxx operations:

  1. {
  2. "_id":"test",
  3. "_rev":"2-b91bb807b4685080c6a651115ff558f5",
  4. "hello":"bar",
  5. "_conflicts":[
  6. "2-65db2a11b5172bf928e3bcf59f728970",
  7. "2-5bc3c6319edf62d4c624277fdd0ae191"
  8. ]
  9. }

If you do GET /db/test?open_revs=all then you will get all the leaf nodes ofthe revision tree. This will give you all the current conflicts, but will alsogive you leaf nodes which have been deleted (i.e. parts of the conflict historywhich have since been resolved). You can remove these by filtering out documentswith "_deleted":true:

  1. [
  2. {"ok":{"_id":"test","_rev":"2-5bc3c6319edf62d4c624277fdd0ae191","hello":"foo"}},
  3. {"ok":{"_id":"test","_rev":"2-65db2a11b5172bf928e3bcf59f728970","hello":"baz"}},
  4. {"ok":{"_id":"test","_rev":"2-b91bb807b4685080c6a651115ff558f5","hello":"bar"}}
  5. ]

The "ok" tag is an artifact of open_revs, which also lets you listexplicit revisions as a JSON array, e.g. open_revs=[rev1,rev2,rev3]. In thisform, it would be possible to request a revision which is now missing, becausethe database has been compacted.

Note

The order of revisions returned by open_revs=all is NOT related tothe deterministic “winning” algorithm. In the above example, the winningrevision is 2-b91b… and happens to be returned last, but in other cases itcan be returned in a different position.

Once you have retrieved all the conflicting revisions, your application can thenchoose to display them all to the user. Or it could attempt to merge them, writeback the merged version, and delete the conflicting versions - that is, toresolve the conflict permanently.

As described above, you need to update one revision and delete all theconflicting revisions explicitly. This can be done using a single POST to_bulk_docs, setting "_deleted":true on those revisions you wish todelete.

5.4.5. Multiple document API

You can fetch multiple documents at once using include_docs=true on a view.However, a conflicts=true request is ignored; the “doc” part of the valuenever includes a _conflicts member. Hence you would need to do another queryto determine for each document whether it is in a conflicting state:

  1. $ curl 'http://127.0.0.1:5984/conflict_test/_all_docs?include_docs=true&conflicts=true'
  1. {
  2. "total_rows":1,
  3. "offset":0,
  4. "rows":[
  5. {
  6. "id":"test",
  7. "key":"test",
  8. "value":{"rev":"2-b91bb807b4685080c6a651115ff558f5"},
  9. "doc":{
  10. "_id":"test",
  11. "_rev":"2-b91bb807b4685080c6a651115ff558f5",
  12. "hello":"bar"
  13. }
  14. }
  15. ]
  16. }
  1. $ curl 'http://127.0.0.1:5984/conflict_test/test?conflicts=true'
  1. {
  2. "_id":"test",
  3. "_rev":"2-b91bb807b4685080c6a651115ff558f5",
  4. "hello":"bar",
  5. "_conflicts":[
  6. "2-65db2a11b5172bf928e3bcf59f728970",
  7. "2-5bc3c6319edf62d4c624277fdd0ae191"
  8. ]
  9. }

5.4.6. View map functions

Views only get the winning revision of a document. However they do also get a_conflicts member if there are any conflicting revisions. This means you canwrite a view whose job is specifically to locate documents with conflicts.Here is a simple map function which achieves this:

  1. function(doc) {
  2. if (doc._conflicts) {
  3. emit(null, [doc._rev].concat(doc._conflicts));
  4. }
  5. }

which gives the following output:

  1. {
  2. "total_rows":1,
  3. "offset":0,
  4. "rows":[
  5. {
  6. "id":"test",
  7. "key":null,
  8. "value":[
  9. "2-b91bb807b4685080c6a651115ff558f5",
  10. "2-65db2a11b5172bf928e3bcf59f728970",
  11. "2-5bc3c6319edf62d4c624277fdd0ae191"
  12. ]
  13. }
  14. ]
  15. }

If you do this, you can have a separate “sweep” process which periodically scansyour database, looks for documents which have conflicts, fetches the conflictingrevisions, and resolves them.

Whilst this keeps the main application simple, the problem with this approach isthat there will be a window between a conflict being introduced and it beingresolved. From a user’s viewpoint, this may appear that the document they justsaved successfully may suddenly lose their changes, only to be resurrected sometime later. This may or may not be acceptable.

Also, it’s easy to forget to start the sweeper, or not to implement it properly,and this will introduce odd behaviour which will be hard to track down.

CouchDB’s “winning” revision algorithm may mean that information drops out of aview until a conflict has been resolved. Consider Bob’s business card again;suppose Alice has a view which emits mobile numbers, so that her telephonyapplication can display the caller’s name based on caller ID. If there areconflicting documents with Bob’s old and new mobile numbers, and they happen tobe resolved in favour of Bob’s old number, then the view won’t be able torecognise his new one. In this particular case, the application might havepreferred to put information from both the conflicting documents into the view,but this currently isn’t possible.

Suggested algorithm to fetch a document with conflict resolution:

  • Get document via GET docid?conflicts=true request
  • For each member in the _conflicts array call GET docid?rev=xxx.If any errors occur at this stage, restart from step 1.(There could be a race where someone else has already resolved this conflictand deleted that rev)
  • Perform application-specific merging
  • Write _bulk_docs with an update to the first rev and deletes of the otherrevs.
    This could either be done on every read (in which case you could replace allcalls to GET in your application with calls to a library which does the above),or as part of your sweeper code.

And here is an example of this in Ruby using the low-level RestClient:

  1. require 'rubygems'
  2. require 'rest_client'
  3. require 'json'
  4. DB="http://127.0.0.1:5984/conflict_test"
  5.  
  6. # Write multiple documents
  7. def writem(docs)
  8. JSON.parse(RestClient.post("#{DB}/_bulk_docs", {
  9. "docs" => docs,
  10. }.to_json))
  11. end
  12.  
  13. # Write one document, return the rev
  14. def write1(doc, id=nil, rev=nil)
  15. doc['_id'] = id if id
  16. doc['_rev'] = rev if rev
  17. writem([doc]).first['rev']
  18. end
  19.  
  20. # Read a document, return *all* revs
  21. def read1(id)
  22. retries = 0
  23. loop do
  24. # FIXME: escape id
  25. res = [JSON.parse(RestClient.get("#{DB}/#{id}?conflicts=true"))]
  26. if revs = res.first.delete('_conflicts')
  27. begin
  28. revs.each do |rev|
  29. res << JSON.parse(RestClient.get("#{DB}/#{id}?rev=#{rev}"))
  30. end
  31. rescue
  32. retries += 1
  33. raise if retries >= 5
  34. next
  35. end
  36. end
  37. return res
  38. end
  39. end
  40.  
  41. # Create DB
  42. RestClient.delete DB rescue nil
  43. RestClient.put DB, {}.to_json
  44.  
  45. # Write a document
  46. rev1 = write1({"hello"=>"xxx"},"test")
  47. p read1("test")
  48.  
  49. # Make three conflicting versions
  50. write1({"hello"=>"foo"},"test",rev1)
  51. write1({"hello"=>"bar"},"test",rev1)
  52. write1({"hello"=>"baz"},"test",rev1)
  53.  
  54. res = read1("test")
  55. p res
  56.  
  57. # Now let's replace these three with one
  58. res.first['hello'] = "foo+bar+baz"
  59. res.each_with_index do |r,i|
  60. unless i == 0
  61. r.replace({'_id'=>r['_id'], '_rev'=>r['_rev'], '_deleted'=>true})
  62. end
  63. end
  64. writem(res)
  65.  
  66. p read1("test")

An application written this way never has to deal with a PUT 409, and isautomatically multi-master capable.

You can see that it’s straightforward enough when you know what you’re doing.It’s just that CouchDB doesn’t currently provide a convenient HTTP API for“fetch all conflicting revisions”, nor “PUT to supersede these N revisions”, soyou need to wrap these yourself. At the time of writing, there are no knownclient-side libraries which provide support for this.

5.4.7. Merging and revision history

Actually performing the merge is an application-specific function. It dependson the structure of your data. Sometimes it will be easy: e.g. if a documentcontains a list which is only ever appended to, then you can perform a union ofthe two list versions.

Some merge strategies look at the changes made to an object, compared to itsprevious version. This is how Git’s merge function works.

For example, to merge Bob’s business card versions v2a and v2b, you could lookat the differences between v1 and v2b, and then apply these changes to v2a aswell.

With CouchDB, you can sometimes get hold of old revisions of a document.For example, if you fetch /db/bob?rev=v2b&revs_info=true you’ll get a listof the previous revision ids which ended up with revision v2b. Doing the samefor v2a you can find their common ancestor revision. However if the databasehas been compacted, the content of that document revision will have been lost.revs_info will still show that v1 was an ancestor, but report it as“missing”:

  1. BEFORE COMPACTION AFTER COMPACTION
  2.  
  3. ,-> v2a v2a
  4. v1
  5. `-> v2b v2b

So if you want to work with diffs, the recommended way is to store those diffswithin the new revision itself. That is: when you replace v1 with v2a, includean extra field or attachment in v2a which says which fields were changed fromv1 to v2a. This unfortunately does mean additional book-keeping for yourapplication.

5.4.8. Comparison with other replicating data stores

The same issues arise with other replicating systems, so it can be instructiveto look at these and see how they compare with CouchDB. Please feel free to addother examples.

5.4.8.1. Unison

Unison is a bi-directional file synchronisation tool. In this case, thebusiness card would be a file, say bob.vcf.

When you run unison, changes propagate both ways. If a file has changed on oneside but not the other, the new replaces the old. Unison maintains a local statefile so that it knows whether a file has changed since the last successfulreplication.

In our example it has changed on both sides. Only one file called _bob.vcf_can exist within the file system. Unison solves the problem by simply duckingout: the user can choose to replace the remote version with the local version,or vice versa (both of which would lose data), but the default action is toleave both sides unchanged.

From Alice’s point of view, at least this is a simple solution. Whenever she’son the desktop she’ll see the version she last edited on the desktop, andwhenever she’s on the laptop she’ll see the version she last edited there.

But because no replication has actually taken place, the data is not protected.If her laptop hard drive dies, she’ll lose all her changes made on the laptop;ditto if her desktop hard drive dies.

It’s up to her to copy across one of the versions manually (under a differentfilename), merge the two, and then finally push the merged version to the otherside.

Note also that the original file (version v1) has been lost at this point.So it’s not going to be known from inspection alone whether v2a or v2b has themost up-to-date E-mail address for Bob, or which version has the most up-to-datemobile number. Alice has to remember which one she entered last.

5.4.8.2. Git

Git is a well-known distributed source control system. Like Unison, Git dealswith files. However, Git considers the state of a whole set of files as a singleobject, the “tree”. Whenever you save an update, you create a “commit” whichpoints to both the updated tree and the previous commit(s), which in turn pointto the previous tree(s). You therefore have a full history of all the states ofthe files. This history forms a branch, and a pointer is kept to the tip of thebranch, from which you can work backwards to any previous state. The “pointer”is an SHA1 hash of the tip commit.

If you are replicating with one or more peers, a separate branch is made foreach of those peers. For example, you might have:

  1. master -- my local branch
  2. remotes/foo/master -- branch on peer 'foo'
  3. remotes/bar/master -- branch on peer 'bar'

In the regular workflow, replication is a “pull”, importing changes froma remote peer into the local repository. A “pull” does two things: first “fetch”the state of the peer into the remote tracking branch for that peer; and thenattempt to “merge” those changes into the local branch.

Now let’s consider the business card. Alice has created a Git repo containingbob.vcf, and cloned it across to the other machine. The branches look likethis, where AAAAAAAA is the SHA1 of the commit:

  1. ---------- desktop ---------- ---------- laptop ----------
  2. master: AAAAAAAA master: AAAAAAAA
  3. remotes/laptop/master: AAAAAAAA remotes/desktop/master: AAAAAAAA

Now she makes a change on the desktop, and commits it into the desktop repo;then she makes a different change on the laptop, and commits it into the laptoprepo:

  1. ---------- desktop ---------- ---------- laptop ----------
  2. master: BBBBBBBB master: CCCCCCCC
  3. remotes/laptop/master: AAAAAAAA remotes/desktop/master: AAAAAAAA

Now on the desktop she does git pull laptop. First, the remote objectsare copied across into the local repo and the remote tracking branch isupdated:

  1. ---------- desktop ---------- ---------- laptop ----------
  2. master: BBBBBBBB master: CCCCCCCC
  3. remotes/laptop/master: CCCCCCCC remotes/desktop/master: AAAAAAAA

Note

The repo still contains AAAAAAAA because commits BBBBBBBB and CCCCCCCCpoint to it.

Then Git will attempt to merge the changes in. Knowing thatthe parent commit to CCCCCCCC is AAAAAAAA, it takes a diff betweenAAAAAAAA and CCCCCCCC and tries to apply it to BBBBBBBB.

If this is successful, then you’ll get a new version with a merge commit:

  1. ---------- desktop ---------- ---------- laptop ----------
  2. master: DDDDDDDD master: CCCCCCCC
  3. remotes/laptop/master: CCCCCCCC remotes/desktop/master: AAAAAAAA

Then Alice has to logon to the laptop and run git pull desktop. A similarprocess occurs. The remote tracking branch is updated:

  1. ---------- desktop ---------- ---------- laptop ----------
  2. master: DDDDDDDD master: CCCCCCCC
  3. remotes/laptop/master: CCCCCCCC remotes/desktop/master: DDDDDDDD

Then a merge takes place. This is a special case: CCCCCCCC is one of theparent commits of DDDDDDDD, so the laptop can fast forward update fromCCCCCCCC to DDDDDDDD directly without having to do any complex merging.This leaves the final state as:

  1. ---------- desktop ---------- ---------- laptop ----------
  2. master: DDDDDDDD master: DDDDDDDD
  3. remotes/laptop/master: CCCCCCCC remotes/desktop/master: DDDDDDDD

Now this is all and good, but you may wonder how this is relevant when thinkingabout CouchDB.

First, note what happens in the case when the merge algorithm fails.The changes are still propagated from the remote repo into the local one, andare available in the remote tracking branch. So, unlike Unison, you know thedata is protected. It’s just that the local working copy may fail to update, ormay diverge from the remote version. It’s up to you to create and commit thecombined version yourself, but you are guaranteed to have all the history youmight need to do this.

Note that while it is possible to build new merge algorithms into Git,the standard ones are focused on line-based changes to source code. They don’twork well for XML or JSON if it’s presented without any line breaks.

The other interesting consideration is multiple peers. In this case you havemultiple remote tracking branches, some of which may match your local branch,some of which may be behind you, and some of which may be ahead of you(i.e. contain changes that you haven’t yet merged):

  1. master: AAAAAAAA
  2. remotes/foo/master: BBBBBBBB
  3. remotes/bar/master: CCCCCCCC
  4. remotes/baz/master: AAAAAAAA

Note that each peer is explicitly tracked, and therefore has to be explicitlycreated. If a peer becomes stale or is no longer needed, it’s up to you toremove it from your configuration and delete the remote tracking branch.This is different from CouchDB, which doesn’t keep any peer state in thedatabase.

Another difference between CouchDB and Git is that it maintains all historyback to timezero - Git compaction keeps diffs between all those versions in order to reducesize, but CouchDB discards them. If you are constantly updating a document,the size of a Git repo would grow forever. It is possible (with some effort)to use “history rewriting” to make Git forget commits earlier than a particularone.

5.4.8.2.1. What is the CouchDB replication protocol? Is it like Git?

Author:
Jason Smith
Date:
——-
2011-01-29
Source:
——-
StackOverflow

Key points

If you know Git, then you know how Couch replication works. Replicating isvery similar to pushing or pulling with distributed source managers like Git.

CouchDB replication does not have its own protocol. A replicator simplyconnects to two DBs as a client, then reads from one and writes to the other.Push replication is reading the local data and updating the remote DB;pull replication is vice versa.

  • Fun fact 1: The replicator is actually an independent Erlang application,in its own process. It connects to both couches, then reads records from oneand writes them to the other.
  • Fun fact 2: CouchDB has no way of knowing who is a normal client and whois a replicator (let alone whether the replication is push or pull).It all looks like client connections. Some of them read records. Some of themwrite records.
    Everything flows from the data model

The replication algorithm is trivial, uninteresting. A trained monkey coulddesign it. It’s simple because the cleverness is the data model, which has theseuseful characteristics:

  • Every record in CouchDB is completely independent of all others. That sucksif you want to do a JOIN or a transaction, but it’s awesome if you want towrite a replicator. Just figure out how to replicate one record, and thenrepeat that for each record.
  • Like Git, records have a linked-list revision history. A record’s revision IDis the checksum of its own data. Subsequent revision IDs are checksums of:the new data, plus the revision ID of the previous.
  • In addition to application data ({"name": "Jason", "awesome": true}),every record stores the evolutionary time line of all previous revision IDsleading up to itself.
    • Exercise: Take a moment of quiet reflection. Consider any two differentrecords, A and B. If A’s revision ID appears in B’s time line, then Bdefinitely evolved from A. Now consider Git’s fast-forward merges.Do you hear that? That is the sound of your mind being blown.
  • Git isn’t really a linear list. It has forks, when one parent has multiplechildren. CouchDB has that too.
    • Exercise: Compare two different records, A and B. A’s revision ID does notappear in B’s time line; however, one revision ID, C, is in both A’s andB’s time line. Thus A didn’t evolve from B. B didn’t evolve from A. Butrather, A and B have a common ancestor C. In Git, that is a “fork.” InCouchDB, it’s a “conflict.”
    • In Git, if both children go on to develop their time lines independently,that’s cool. Forks totally support that.
    • In CouchDB, if both children go on to develop their time linesindependently, that cool too. Conflicts totally support that.
    • Fun fact 3: CouchDB “conflicts” do not correspond to Git “conflicts.”A Couch conflict is a divergent revision history, what Git calls a “fork.”For this reason the CouchDB community pronounces “conflict” with a silentn: “co-flicked.”
  • Git also has merges, when one child has multiple parents. CouchDB sort ofhas that too.
    • In the data model, there is no merge. The client simply marks onetime line as deleted and continues to work with the only extant time line.
    • In the application, it feels like a merge. Typically, the client mergesthe data from each time line in an application-specific way.Then it writes the new data to the time line. In Git, this is like copyingand pasting the changes from branch A into branch B, then committing tobranch B and deleting branch A. The data was merged, but there was nogit merge.
    • These behaviors are different because, in Git, the time line itself isimportant; but in CouchDB, the data is important and the time line isincidental—it’s just there to support replication. That is one reason whyCouchDB’s built-in revisioning is inappropriate for storing revision datalike a wiki page.
      Final notes

At least one sentence in this writeup (possibly this one) is complete BS.

原文: http://docs.couchdb.org/en/stable/replication/conflicts.html