| |
| = How Subversion's conflict resolver handles incoming moves = |
| |
| Given a victim of a tree conflict which may involve an incoming move, |
| the conflict resolver must solve these problems: |
| |
| 1) Find all moves in the log within the operative revision range recorded |
| in conflict meta data. Move detection is based on changed-paths data |
| in the revision log. |
| |
| 2) If the conflict victim is not in the working copy, find a path in the |
| working copy which corresponds to the current repository location of |
| the conflict victim. |
| |
| 3) Find the conflict victim's counterpart and its path-wise history. |
| |
| 4) Determine which, if any, moves apply to the conflict victim's counterpart. |
| |
| The result of incoming move detection describes all moves affecting |
| the conflict victim's counterpart, and by extension the victim itself, |
| within the operative revision range recorded in conflict data. |
| |
| The result allows the conflict resolver to offer options which resolve the |
| tree conflict by merging appropriate changes from the victim's counterpart |
| to a node in the working copy which represents the conflict victim. |
| |
| == Node == |
| |
| A node is a versioned file or a directory as represented in an SVN repository. |
| |
| == Node relatedness == |
| |
| Given a node at path A at revision rX, and a node at path B at revision rY, |
| the two nodes are related if the history of both nodes can be traced back |
| to a single path C at revision rZ. |
| |
| == Finding moves in a given revision == |
| |
| In the revision log, moves are represented by disjoint copy and delete |
| operations. |
| |
| Since Subversion 1.8, SVN clients enforce that after 'svn move A B', the |
| deletion of A and the addition of B (as a copy of A) must be committed in |
| the same revision. |
| Move detection in the conflict resolver relies on this enforcement. |
| |
| === "Direct" moves === |
| |
| Direct moves appear with the following pattern: |
| |
| - A changed path P is deleted in rN. |
| - A changed path Q appears in rN and is a copy of P at or after |
| the last-changed revision of P@r{N-1}. |
| - There is no other changed path R which is also a copy of P at |
| or after the last-changed revison of P@r{N-1}. |
| |
| Example: |
| |
| In the most simple case, a file '^/trunk/alpha' moved in r3 would appear |
| in the output of 'svn log -v' as: |
| |
| Changed paths: |
| D /trunk/alpha |
| A /trunk/alpha-moved (from /trunk/alpha:2) |
| |
| This same pattern would also be recognized as a move if it appeared in, |
| say, r6, provided that the last-changed revision of ^/trunk/alpha@5 is |
| still smaller or equal r2. |
| |
| === "Ambiguous" moves === |
| |
| Ambiguous moves appear with the following pattern: |
| |
| - A changed path P is deleted in rN. |
| - A changed path Q1 appears in rN and is a copy of P at or after |
| the last-changed revision of P@r{N-1}. |
| - One or more changed paths Q[2,3,4,...] appear in rN as a copy of P |
| at or after the last-changed revision of P@r{N-1}. |
| |
| Example: |
| |
| If a file '^/trunk/alpha' was copied twice and then moved in r3, |
| this would appear in the output of 'svn log -v' as: |
| |
| Changed paths: |
| D /trunk/alpha |
| A /trunk/alpha-copied1 (from /trunk/alpha:2) |
| A /trunk/alpha-copied2 (from /trunk/alpha:2) |
| A /trunk/alpha-moved (from /trunk/alpha:2) |
| |
| In such situations, SVN cannot tell whether any of alpha's copies should |
| in fact be treated as a move. |
| Ambiguous moves require user interaction. During conflict resolution the |
| user must pick the move destination from a set of candidates. |
| |
| === "Nested" moves === |
| |
| Nested moves appear with the following pattern: |
| |
| - A direct or ambiguous move M appears in rN. |
| - A path P is deleted and P is a path-wise child of the copied path |
| which belongs to move M. |
| - A new path Q is added which was copied from the location P would have had |
| in absence of the move M, at or after the last-changed revision of P@{N-1}. |
| |
| Examples: |
| |
| If a directory 'gamma' was moved in r3 and the child 'gamma/delta' was |
| also moved in r3, this would appear in the output of 'svn log -v' as: |
| |
| Changed paths: |
| D /trunk/gamma |
| A /trunk/gamma-moved (from /trunk/gamma:2) |
| D /trunk/gamma-moved/delta |
| A /trunk/gamma-moved/delta-moved (from /trunk/gamma/delta:2) |
| |
| If the child was moved outside its parent instead of within its parent, |
| the output might look like: |
| |
| Changed paths: |
| A /trunk/epsilon/delta-moved (from /trunk/gamma/delta:2) |
| D /trunk/gamma |
| A /trunk/gamma-moved (from /trunk/gamma:2) |
| D /trunk/gamma-moved/delta |
| |
| Nested moves inside nested moves are also possible. Again, added nodes appear |
| as copied from locations they would have had in the absence of any other moves: |
| |
| Changed paths: |
| D /trunk/gamma |
| A /trunk/gamma-moved (from /trunk/gamma:4) |
| D /trunk/gamma-moved/psi |
| A /trunk/gamma-moved/psi-moved (from /trunk/gamma/psi:4) |
| D /trunk/gamma-moved/psi-moved/omega |
| A /trunk/omega-moved (from /trunk/gamma/psi/omega:4) |
| |
| == Finding a particular move == |
| |
| Repository nodes the update/merge/switch editor was working with are |
| recorded in conflict meta data: |
| - path@old-rev, old-node-kind |
| - path@new-rev, new-node-kind |
| The repos-path@rev is for the conflict victim is also available, as it |
| appeared in the working copy at the time the conflict was flagged. |
| |
| To determine whether path@old-rev was moved to another path-moved@new-rev, |
| SVN must look for a chain of moves which starts at path@old-rev and ends at |
| new-rev. If a single such chain is found, then the final path-moved@new-rev |
| is known. |
| |
| To do this, SVN scans all revisions between old-rev and new-rev and |
| detects any moves within them. Moves of the same node are linked together |
| and form move chains. If ambiguous moves are found in a revision, move |
| chains may diverge into several directions at that revision. |
| |
| If nested moves are found in a revision, they end up being represented |
| the same way as direct moves are (so they are no longer a special case |
| from this point onwards). |
| |
| == Finding missing conflict victims == |
| |
| The repository location of the conflict victim is unkown if the victim |
| cannot be found in the working copy ("local missing"). |
| |
| To find such missing nodes, SVN must first find all moves in the entire |
| history of the parent directory of the conflict victim. Additionally, |
| in the case of a merge operation, SVN must also find all moves in the |
| history of the parent directory of path@old-rev, all the way up to the |
| common ancestor of the root of the merge operation. This is because such |
| moves might not have been applied to the target branch (working copy), |
| for instance when cherry-picking a file modification, after the file has |
| been moved on the source branch (see example at the end of this section). |
| |
| ### jcorvel: Maybe this additional search for moves on the source branch |
| (in case of a merge operation) can be optional? Only do it if |
| the moves found in the first search don't suffice? |
| |
| For each such move it checks whether the moved node is related to the known |
| node at path@old-rev, or, if that does not exist, path@new-rev, by tracing |
| backwards in history from path@old-rev/new-rev if the move's revision is |
| smaller than old-rev/new-rev, or by tracing backwards in history from the |
| moved path if the move's revision is larger than old-rev/new-rev. |
| |
| For any such related node's repository path at revision new-rev recorded in the |
| conflict, a local path in the working copy is searched which is related to this |
| repository path. Any such nodes found in the working copy are candidates for |
| the missing conflict victim's current location, unless the node is inside a |
| switched subtree or is itself a switched node or is an external. |
| |
| If multiple matches are found the user must be given a choice with a default |
| suggestion. To avoid choosing bad default suggestions in cases where multiple |
| branches are checked out into a working copy (such as in SVN's own test suite), |
| a path-wise closest node to the conflict victim is the preferred suggestion. |
| |
| If no such node can be found, SVN assumes that the conflict victim was |
| deleted instead of moved. |
| |
| === Missing conflict victim due to skipped move in merge source history === |
| |
| This can typically happen when cherry-picking a revision with a file |
| modification, where this file has been moved on the source branch of the |
| merge (and this move was not applied to the target branch): |
| |
| In r1, create directory A with file mu: |
| Changed paths: |
| A /A |
| A /A/mu |
| |
| In r2, directory A is copied to A1 (branched): |
| Changed paths: |
| A /A1 (from /A:1) |
| |
| In r3, A/mu is moved: |
| Changed paths: |
| D /A/mu |
| A /A/mu-moved (from /A/mu:2) |
| |
| In r4, A/mu-moved is edited: |
| Changed paths: |
| M /A/mu-moved |
| |
| If we now want to cherry-pick r4 from /A to a working copy of /A1, we get a |
| tree conflict because mu-moved is missing. The relevant move we need to |
| resolve this happened on /A, in r3. |
| |
| == Determining which, if any, moves apply == |
| |
| Next, SVN must determine whether any moves found between old-rev and |
| new-rev link path@old-rev to an path-moved@new-rev, and whether |
| path-moved@new-rev is related to the conflict victim. |
| |
| A move of path A to path B in rN applies to a path P@N if P is a path-wise |
| child of, or equal to, A. |
| |
| == Special considerations for reverse operations == |
| |
| ... TODO talk about reverse-updates and -merges, and switches to older revs ... |