Layered Moves
The idea is to store moved_to in the presence=base-deleted node, rather than the BASE node. If/when the node is replaced and becomes presence=normal the moved_to still refers to the node that was base-deleted.
op-depth |
local_relpath |
presence |
moved_to |
0 |
A |
normal |
|
0 |
A/B |
normal |
|
0 |
A/B/C |
normal |
|
0 |
A/B/C/D |
normal |
|
|
table A1 |
|
|
Move A/B/C/D to X
op-depth |
local_relpath |
presence |
moved_to |
0 |
A |
normal |
|
0 |
A/B |
normal |
|
0 |
A/B/C |
normal |
|
0 |
A/B/C/D |
normal |
|
4 |
A/B/C/D |
base-deleted |
X |
|
table A2 |
|
|
Move A/B to Y
op-depth |
local_relpath |
presence |
moved_to |
0 |
A |
normal |
|
0 |
A/B |
normal |
|
0 |
A/B/C |
normal |
|
0 |
A/B/C/D |
normal |
|
2 |
A/B |
base-deleted |
Y |
2 |
A/B/C |
base-deleted |
|
2 |
A/B/C/D |
base-deleted |
X |
|
table A3 |
|
|
Replace A/B with a copy that includes a C/D
op-depth |
local_relpath |
presence |
moved_to |
0 |
A |
normal |
|
0 |
A/B |
normal |
|
0 |
A/B/C |
normal |
|
0 |
A/B/C/D |
normal |
|
2 |
A/B |
normal |
Y |
2 |
A/B/C |
normal |
|
2 |
A/B/C/D |
normal |
X |
|
table A4 |
|
|
Move A/B/C to Z
op-depth |
local_relpath |
presence |
moved_to |
0 |
A |
normal |
|
0 |
A/B |
normal |
|
0 |
A/B/C |
normal |
|
0 |
A/B/C/D |
normal |
|
2 |
A/B |
normal |
Y |
2 |
A/B/C |
normal |
|
2 |
A/B/C/D |
normal |
X |
3 |
A/B/C |
base-deleted |
Z |
3 |
A/B/C/D |
base-deleted |
|
|
table A5 |
|
|
Replace A/B/C with a copy that includes D
op-depth |
local_relpath |
presence |
moved_to |
0 |
A |
normal |
|
0 |
A/B |
normal |
|
0 |
A/B/C |
normal |
|
0 |
A/B/C/D |
normal |
|
2 |
A/B |
normal |
Y |
2 |
A/B/C |
normal |
|
2 |
A/B/C/D |
normal |
X |
3 |
A/B/C |
normal |
Z |
3 |
A/B/C/D |
normal |
|
|
table A6 |
|
|
Move A/B/C/D to Q
op-depth |
local_relpath |
presence |
moved_to |
0 |
A |
normal |
|
0 |
A/B |
normal |
|
0 |
A/B/C |
normal |
|
0 |
A/B/C/D |
normal |
|
2 |
A/B |
normal |
Y |
2 |
A/B/C |
normal |
|
2 |
A/B/C/D |
normal |
X |
3 |
A/B/C |
normal |
Z |
3 |
A/B/C/D |
normal |
|
4 |
A/B/C/D |
base-deleted |
Q |
|
table A7 |
|
|
Now scan_deletion(A/B/C/D) needs to tell us moved_to is Q at op-depth=4, Z/D at op-depth=3, X at op-depth=2. Quite how it does this is an open question. Does the caller pass the op-depth? Where would the caller get it? Does the function return some sort of array of (op-depth, moved-to) pairs?
Nested Moves
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
|
table B1 |
|
|
Move A to B and then B/F to B/G
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
1 |
A |
base-deleted |
B |
1 |
A/F |
base-deleted |
|
1 |
B |
normal |
|
1 |
B/F |
normal |
|
|
table B2 |
|
|
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
1 |
A |
base-deleted |
B |
1 |
A/F |
base-deleted |
|
1 |
B |
normal |
|
1 |
B/F |
normal |
|
2 |
B/F |
base-deleted |
B/G |
2 |
B/G |
normal |
|
|
table B3 |
|
|
Alternatively, move A/F to A/G and then A to B
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
2 |
A/F |
base-deleted |
A/G |
2 |
A/G |
normal |
|
|
table B4 |
|
|
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
1 |
A |
base-deleted |
B |
1 |
A/F |
base-deleted |
|
1 |
B |
normal |
|
1 |
B/F |
normal |
|
2 |
B/F |
base-deleted |
B/G |
2 |
B/G |
normal |
|
|
table B5 |
|
|
The final database state is the same independent of the order of the moves. The second move in the second case causes the explicit moved-to associated with A/F to be removed.
Copy compared to Move
Now consider copying A instead of moving it, so copy A to C then move C/F to C/G.
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
1 |
C |
normal |
|
1 |
C/F |
normal |
|
2 |
C/F |
base-deleted |
C/G |
2 |
C/G |
normal |
|
|
table C1 |
|
|
There is only one move recorded C/F->C/G. The move inside the copy is recorded in a similar way to the move inside a move. The move can happen before the copy:
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
2 |
A/F |
base-deleted |
A/G |
2 |
A/G |
normal |
|
|
table C2 |
|
|
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
2 |
A/F |
base-deleted |
A/G |
2 |
A/G |
normal |
|
1 |
C |
normal |
|
1 |
C/F |
normal |
|
2 |
C/F |
base-deleted |
C/G |
2 |
C/G |
normal |
|
|
table C3 |
|
|
Irrespective of the order of the copy and move operations we end up with moved-to for C/F to C/G.
Move rewrites moved-to
Move has to rewrite moved-to both inside an outside the moved tree. The first case is moved-to entirely within the moved tree: move A to B after A/F to A/G. This causes moved-to on A/F to be rewritted (to null) and moved-to on B/F to be added:
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
2 |
A/F |
base-deleted |
A/G |
2 |
A/G |
normal |
|
|
table D1 |
|
|
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
1 |
A |
base-deleted |
B |
1 |
A/F |
base-deleted |
|
1 |
B |
normal |
|
1 |
B/F |
normal |
|
2 |
B/F |
base-deleted |
B/G |
2 |
B/G |
normal |
|
|
table D2 |
|
|
The second case is moved-to from inside the moved-tree to outside the moved tree: move A to B after A/F to G. Like the first case this requires the moved-to on A/F to be rewritten (to null) and moved-to on B/F to be added. Compared to the first case the value of the rewritten moved-to doesn't change:
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
2 |
A/F |
base-deleted |
G |
1 |
G |
normal |
|
|
table D3 |
|
|
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
A/F |
normal |
|
1 |
A |
base-deleted |
B |
1 |
A/F |
base-deleted |
|
1 |
B |
normal |
|
1 |
B/F |
normal |
|
2 |
B/F |
base-deleted |
G |
1 |
G |
normal |
|
|
table D4 |
|
|
The third case is move-to from outside the moved tree into the moved tree: move A to B after F to A/F. This causes moved-to on F, outside the moved tree, to be rewritten:
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
F |
normal |
|
1 |
F |
base-deleted |
A/G |
2 |
A/G |
normal |
|
|
table D5 |
|
|
op-depth |
local-relpath |
presence |
moved-to |
0 |
A |
normal |
|
0 |
F |
normal |
|
1 |
A |
base-deleted |
B |
1 |
F |
base-deleted |
B/G |
1 |
B |
normal |
|
2 |
B/G |
normal |
|
|
table D6 |
|
|
No Mixed-Revision Moves
Subversion does support mixed-revision copies but moves will be restricted to single-revision trees. This doesn't restrict users in practice since these are the only sort of moves that can be committed. A mixed revision copy:
op-depth |
local-relpath |
presence |
revision |
0 |
A |
normal |
4 |
0 |
A/B |
normal |
6 |
1 |
X |
normal |
4 |
1 |
X/B |
not-present |
|
2 |
X/B |
normal |
6 |
|
table E1 |
|
|
and two nested copies:
op-depth |
local-relpath |
presence |
revision |
0 |
A |
normal |
4 |
0 |
A/B |
normal |
6 |
1 |
X |
normal |
4 |
1 |
X/B |
normal |
4 |
2 |
X/B |
normal |
6 |
|
table E2 |
|
|
both get committed as two copies:
A /X (from /A:4) |
A /X/B (from /A/B:6) |
If the revisions later than r4 do not affect the /A tree then we can also commit the delete of /A but the result doesn't look like a mixed-revision move:
D /A |
A /X (from /A:4) |
A /X/B (from /A/B:6) |
This is a move of /A with an additional copy inside the move destination. Moves are essentially single revision in the repository: the same node gets deleted and copied and the deleted node has to be single-revision.
It will be possible to move single-revision trees with local modifications, e.g. a tree A with a locally added A/G:
op-depth |
local-relpath |
presence |
moved-to |
moved-here |
0 |
A |
normal |
|
|
0 |
A/F |
normal |
|
|
2 |
A/G |
normal |
|
|
|
|
table E3 |
|
|
Move A to B gives a single-revision move of A-to-B with a locally added B/G:
op-depth |
local-relpath |
presence |
moved-to |
moved-here |
0 |
A |
normal |
|
|
0 |
A/F |
normal |
|
|
1 |
A |
base-deleted |
B |
|
1 |
A/F |
base-deleted |
|
|
1 |
B |
normal |
|
1 |
1 |
B/F |
normal |
|
1 |
2 |
B/G |
normal |
|
|
|
|
table E4 |
|
|
Updating a Move
When a single-revision BASE tree is updated from one revision to another it temporarily becomes mixed revision (for both v1 and v2 editors). We cannot use the mixed-revision copy representation for these mixed-revision states as mixed-revision copies are represented as multiple-op-depth nested copies. The use of multiple-op-depth for mixed-revision moves would interfere with the use of multiple-op-depth for copies inside move destinations.
A typical move of A/B to X looks like:
op-depth |
local-relpath |
presence |
revision |
moved-to |
moved-here |
0 |
A |
normal |
6 |
|
|
0 |
A/B |
normal |
6 |
|
|
0 |
A/B/C |
normal |
6 |
|
|
2 |
A/B |
base-deleted |
|
X |
|
2 |
A/B/C |
base-deleted |
|
|
|
1 |
X |
normal |
6 |
|
1 |
1 |
X/C |
normal |
6 |
|
1 |
|
|
table A1 |
|
|
|
If the working copy is updated the base tree changes, say from A@6 to A@8 where something inside the deleted tree changes:
op-depth |
local-relpath |
presence |
revision |
moved-to |
moved-here |
0 |
A |
normal |
8 |
|
|
0 |
A/B |
normal |
8 |
|
|
0 |
A/B/C |
normal |
8 |
|
|
0 |
A/B/D |
normal |
|
|
|
2 |
A/B |
base-deleted |
|
X |
|
2 |
A/B/C |
base-deleted |
|
|
|
1 |
X |
normal |
6 |
|
1 |
1 |
X/C |
normal |
6 |
|
1 |
|
|
table A2 |
|
|
|
At present this produces a tree-conflict on A/B since the changes made to A/B are in the deleted tree. It also "breaks" the move since the move source and destination revisions no longer match. To "fix" the move the destination has to be modified to match the source. This involves changing the revsion of X and X/C, adding new nodes like X/D and removing deleted nodes (none in this example).
op-depth |
local-relpath |
presence |
revision |
moved-to |
moved-here |
0 |
A |
normal |
8 |
|
|
0 |
A/B |
normal |
8 |
|
|
0 |
A/B/C |
normal |
8 |
|
|
0 |
A/B/D |
normal |
|
|
|
2 |
A/B |
base-deleted |
|
X |
|
2 |
A/B/C |
base-deleted |
|
|
|
1 |
X |
normal |
8 |
|
1 |
1 |
X/C |
normal |
8 |
|
1 |
1 |
X/D |
normal |
8 |
|
1 |
|
|
table A3 |
|
|
|
We also have to consider updating the working files. File that are unchanged between r6 and r8 need no update. If there are changes between r6 and r8 and the working file is unmodified then the working file can simply be replaced, but if the working file is modified then the r6 to r8 changes need to merged into the working file. Changes to the working file need to be made via the workqueue mechanism so that they happen if and only if the NODES row changes.
Resolving a Conflict Produces New Conflicts
When the move update needs to add or delete rows from the destination, rather than simply updating the revision, that may lead to new tree conflicts. Suppose I start with table A1 and add X/D:
op-depth |
local-relpath |
presence |
revision |
moved-to |
moved-here |
0 |
A |
normal |
6 |
|
|
0 |
A/B |
normal |
6 |
|
|
0 |
A/B/C |
normal |
6 |
|
|
2 |
A/B |
base-deleted |
|
X |
|
2 |
A/B/C |
base-deleted |
|
|
|
1 |
X |
normal |
6 |
|
1 |
1 |
X/C |
normal |
6 |
|
1 |
2 |
X/D |
normal |
|
|
|
|
|
table C1 |
|
|
|
After the move destination is updated X/D will be 'R'eplaced:
op-depth |
local-relpath |
presence |
revision |
moved-to |
moved-here |
0 |
A |
normal |
8 |
|
|
0 |
A/B |
normal |
8 |
|
|
0 |
A/B/C |
normal |
8 |
|
|
0 |
A/B/D |
normal |
|
|
|
2 |
A/B |
base-deleted |
|
X |
|
2 |
A/B/C |
base-deleted |
|
|
|
1 |
X |
normal |
8 |
|
1 |
1 |
X/C |
normal |
8 |
|
1 |
1 |
X/D |
normal |
8 |
|
1 |
2 |
X/D |
normal |
|
|
|
|
|
table C2 |
|
|
|