How the conflict resolver deals with incoming moves
An incoming move during an update/switch/merge operation appears as disjoint additions and deletions of working copy nodes.
The conflict resolver scans the revision log to match up copies and deletions. See 'struct repos_move_info' and 'find_moves_in_revision()' in the file subversion/libsvn_client/conflicts.c. While this approach is not perfect (it does not correctly handle deletions which happened inside copies, for instance) it is good enough to support users during conflict resolution in many use cases.
Incoming move during updates
Consider the following pre-update NODES table:
op_depth |
local_relpath |
presence |
0 |
A |
normal |
0 |
A/f |
normal |
with a locally modified file A/f.
After A is moved to B in the repository the user updates to the HEAD revision. The NODES table will now look as follows:
op_depth |
local_relpath |
presence |
0 |
B |
normal |
0 |
B/f |
normal |
1 |
A |
normal |
1 |
A/f |
normal |
The copy op-root A was created when a local edit vs incoming delete tree conflict was raised on A. This is how the update process attempts to preserve the user's local changes. The conflict resolver relies on this behaviour.
The ACTUAL_NODE table now stores a tree conflict on A (the conflict skel shown here is heavily abbreviated):
local_relpath |
tree_conflict_data |
A |
((update ((trunk/A 2 dir) (trunk/A 3 none))) ((tree () edited deleted))) |
The resolver scans the revision log and determines that A was moved to B. Merging the incoming move involves merging all changes within conflict victim A into the newly added subtree B.
In our simple example, this means running a 3-way merge of the files pristine(A/f), A/f, and B/f, into B/f. Afterwards, the rows for A can be deleted from the NODES table and the conflict skel for A can be removed:
op_depth |
local_relpath |
presence |
0 |
B |
normal |
0 |
B/f |
normal |
Things get more interesting if local changes are present in the NODES table before the update, such as an added file n and a deleted file d:
op_depth |
local_relpath |
presence |
0 |
A |
normal |
0 |
A/f |
normal |
0 |
A/d |
normal |
2 |
A/n |
normal |
2 |
A/d |
base-deleted |
The post-update state is:
op_depth |
local_relpath |
presence |
0 |
B |
normal |
0 |
B/f |
normal |
0 |
B/d |
normal |
1 |
A |
normal |
1 |
A/f |
normal |
1 |
A/d |
normal |
2 |
A/n |
normal |
2 |
A/d |
base-deleted |
Merging the incoming move involves moving all changes within conflict victim A into the newly added subtree B and removing the rows for A.
This operation must be performed by an editor which edits the B subtree. It refers to the A subtree at op_depth 1 (actually relpath_depth('A')) as the base tree, and to the A subtree at op_depth > 1 as the changed tree. Any nodes at op_depth > 1 trigger corresponding edits in the B subtree.
The result of this edit should be:
op_depth |
local_relpath |
presence |
0 |
B |
normal |
0 |
B/f |
normal |
0 |
B/d |
normal |
2 |
B/n |
normal |
2 |
B/d |
base-deleted |
File contents within A from the pristine store are part of the editor's base tree, while the working files within A on disk are part of the changed tree. This results in the previously discussed 3-way merge of pristine(A/f), A/f, and B/f, into B/f.
Any nodes within the B subtree at op-depths > 1 trigger a tree conflict. Suppose the user adds 'B/n' and moves 'B/d' to 'B/c' before running the conflict resolver:
op_depth |
local_relpath |
presence |
moved_to |
moved_here |
0 |
B |
normal |
|
|
0 |
B/f |
normal |
|
|
0 |
B/d |
normal |
|
|
2 |
B/n |
normal |
|
|
2 |
B/d |
base-deleted |
B/c |
|
2 |
B/c |
normal |
|
1 |
1 |
A |
normal |
|
|
1 |
A/f |
normal |
|
|
1 |
A/d |
normal |
|
|
2 |
A/n |
normal |
|
|
2 |
A/d |
base-deleted |
|
|
While editing the B tree, the resolver could now create tree conflicts for B/n (local add vs incoming add) and B/d (local move vs incoming deletion) in the ACTUAL_NODE table. This raises several new problems:
The meaning of the terms 'local' and 'incoming' in conflict descriptions is now misleading because both of the conflicting tree changes were made in the working copy ('local').
The resolver will have to be taught to resolve such conflicts (this is not impossible but implies extra implementation effort).
The resolver will have to choose whether the existing node in B should be left as-is, or whether it should be replaced with the node from A. Either way, information is lost unless the rows for conflicting nodes are left unmodified in both A and B.
The resolver could also reject conflict resolution if local changes are present in the B tree. However, this approach would work only for updates but not for merges.
Incoming move during merges
For a merge from trunk to a branch, the pre-merge NODES table might look like:
op_depth |
local_relpath |
presence |
repos_path |
0 |
A |
normal |
branch/A |
0 |
A/f |
normal |
branch/A/f |
where file A/f differs from the corresponding file on the trunk (for now, let's assume the merge target working copy contains no local modifications).
While running a merge from the trunk, the repository-side move of A to B results in the deletion of A and the addition of B as a copy from the trunk. The NODES now table looks like:
op_depth |
local_relpath |
presence |
repos_path |
0 |
A |
normal |
branch/A |
0 |
A/f |
normal |
branch/A/f |
1 |
B |
normal |
trunk/B |
1 |
B/f |
normal |
trunk/B/f |
And the ACTUAL_NODE table stores a tree conflict marker for A:
local_relpath |
tree_conflict_data |
A |
((merge ((trunk/A 1 dir) (trunk/A 4 none))) ((tree () edited deleted))) |
The conflict resolver scans the log and determines that A has been renamed to B.
The resolver must now run a 3-way merge of YCA(A/f, B/f), A/f, and B/f, into B/f. To do this, the youngest common ancestor of A/f and B/f must be fetched from the repository and stored in a temporary file (the YCA content could already be present in the pristine store in some cases, but making use of this fact is an optimization).
Let's examine the situation with a pre-merge NODES table that has already committed tree changes relative to the trunk: a deleted file A/d and a new file A/n:
op_depth |
local_relpath |
presence |
repos_path |
0 |
A |
normal |
branch/A |
0 |
A/f |
normal |
branch/A/f |
0 |
A/n |
normal |
branch/A/n |
The post-merge state would look like this:
op_depth |
local_relpath |
presence |
repos_path |
0 |
A |
normal |
branch/A |
0 |
A/f |
normal |
branch/A/f |
0 |
A/n |
normal |
branch/A/n |
1 |
B |
normal |
trunk/B |
1 |
B/d |
normal |
trunk/B/d |
Merging the incoming move involves moving all changes within conflict victim A into the newly added subtree B, and marking a move of A to B in the NODES table.
Part of this operation must be performed by an editor which edits the B subtree. It refers to the repository-side A subtree at the common-ancestor revision rYCA(A,B) as the base tree, and to the repository-side A subtree at the BASE revision of A as the changed tree. Any nodes which have changed since rYCA trigger edits in the B subtree. This can be implemented by running the equivalent of a standard merge
-rYCA:BASE ^/branch/A@BASE into B.
This results in:
op_depth |
local_relpath |
presence |
repos_path |
0 |
A |
normal |
branch/A |
0 |
A/f |
normal |
branch/A/f |
0 |
A/n |
normal |
branch/A/n |
1 |
B |
normal |
trunk/B |
1 |
B/d |
normal |
trunk/B/d |
1 |
B/f |
normal |
trunk/B/f |
2 |
B/d |
base-deleted |
|
2 |
B/n |
normal |
branch/A/n |
Marking a move in the working copy requires adding a base-deleted row for A with a moved_to column which points at B and setting B's moved_here flag:
op_depth |
local_relpath |
presence |
repos_path |
moved_to |
moved_here |
0 |
A |
normal |
branch/A |
|
|
0 |
A/f |
normal |
branch/A/f |
|
|
0 |
A/n |
normal |
branch/A/n |
|
|
1 |
A |
base_deleted |
|
B |
|
1 |
B |
normal |
trunk/B |
|
1 |
1 |
B/d |
normal |
trunk/B/d |
|
|
1 |
B/f |
normal |
trunk/B/f |
|
|
2 |
B/d |
base-deleted |
|
|
|
2 |
B/n |
normal |
branch/A/n |
|
|
However, B's repository path still points to the trunk. Only newly added children within B appear as copied from the branch.
A better the resolution result would be a move of A to B on the branch:
op_depth |
local_relpath |
presence |
repos_path |
moved_to |
moved_here |
0 |
A |
normal |
branch/A |
|
|
0 |
A/f |
normal |
branch/A/f |
|
|
0 |
A/n |
normal |
branch/A/n |
|
|
1 |
A |
base_deleted |
|
B |
|
1 |
B |
normal |
branch/A |
|
1 |
1 |
B/f |
normal |
branch/A/f |
|
|
1 |
B/n |
normal |
branch/A/n |
|
|
With changes from the trunk merged in.
This could be achieved by first reverting B, then moving A to B locally, and running the appropriate merges to get changes from trunk. However, the resolver has no way of knowing whether reverting B would destroy any local changes the user made to B after the merge. Ideally, such changes would be retained.