7. 处理结构性冲突

So far, we have only talked about conflicts at the level of file content. When you and your collaborators make overlapping changes within the same file, Subversion forces you to merge those changes before you can commit.[9]

But what happens if your collaborators move or delete a file that you are still working on? Maybe there was a miscommunication, and one person thinks the file should be deleted, while another person still wants to commit changes to the file. Or maybe your collaborators did some refactoring, renaming files and moving around directories in the process. If you were still working on these files, those modifications may need to be applied to the files at their new location. Such conflicts manifest themselves at the directory tree structure level rather than at the file content level, and are known as tree conflicts.

As with textual conflicts, tree conflicts prevent a commit from being made from the conflicted state, giving the user the opportunity to examine the state of the working copy for potential problems arising from the tree conflict, and resolving any such problems before committing.

7.1. An Example Tree Conflict

假定你正在工作的软件项目布局如下:

$ svn list -Rv svn://svn.example.com/trunk/
      4 harry                 Feb 06 14:34 ./
      4 harry              23 Feb 06 14:34 COPYING
      4 harry              41 Feb 06 14:34 Makefile
      4 harry              33 Feb 06 14:34 README
      4 harry                 Feb 06 14:34 code/
      4 harry              51 Feb 06 14:34 code/bar.c
      4 harry             124 Feb 06 14:34 code/foo.c

你的协作者 Harry 已经将 bar.c 改名为 baz.c。你仍旧使用 bar.c 工作,不知道它在版本库中已经改名。

Harry 的提交日志是:

$ svn log -r5 svn://svn.example.com/trunk
------------------------------------------------------------------------
r5 | harry | 2009-02-06 14:42:59 +0000 (Fri, 06 Feb 2009) | 2 lines
Changed paths:
   M /trunk/Makefile
   D /trunk/code/bar.c
   A /trunk/code/baz.c (from /trunk/code/bar.c:4)

Rename bar.c to baz.c, and adjust Makefile accordingly.

你的本地修改是:

$ svn diff
Index: code/foo.c
===================================================================
--- code/foo.c  (revision 4)
+++ code/foo.c  (working copy)
@@ -3,5 +3,5 @@
 int main(int argc, char *argv[])
 {
        printf("I don't like being moved around!\n%s", bar());
-       return 0;
+       return 1;
 }
Index: code/bar.c
===================================================================
--- code/bar.c  (revision 4)
+++ code/bar.c  (working copy)
@@ -1,4 +1,4 @@
 const char *bar(void)
 {
-       return "Me neither!\n";
+       return "Well, I do like being moved around!\n";
 }

Your changes are all based on revision 4. They cannot be committed because Harry has already checked in revision 5:

$ svn commit -m "Small fixes"
Sending        code/bar.c
Sending        code/foo.c
Transmitting file data ..
svn: Commit failed (details follow):
svn: File not found: transaction '5-5', path '/trunk/code/bar.c'

这时候你需要执行 svn update 命令。此外,更新工作目录后你可以看到 Harry 的修改,发现产生了树冲突,从而你有机会评估和正确处理它。

$ svn update
   C code/bar.c
A    code/baz.c
U    Makefile
Updated to revision 5.
Summary of conflicts:
  Tree conflicts: 1

在输出中,svn update 在第四列使用大写字母 C 标记树冲突。svn status 展现了冲突的额外信息:

$ svn status
M       code/foo.c
A  +  C code/bar.c
      >   local edit, incoming delete upon update
M       code/baz.c

Note how bar.c is automatically scheduled for re-addition in your working copy, which simplifies things in case you want to keep the file.

Because a move in Subversion is implemented as a copy operation followed by a delete operation, and these two operations cannot be easily related to one another during an update, all Subversion can warn you about is an incoming delete operation on a locally modified file. This delete operation may be part of a move, or it could be a genuine delete operation. Talking to your collaborators, or, as a last resort, svn log, is a good way to find out what has actually happened.

Both foo.c and baz.c are reported as locally modified in the output of svn status. You made the changes to foo.c yourself, so this should not be surprising. But why is baz.c reported as locally modified?

The answer is that despite the limitations of the move implementation, Subversion was smart enough to transfer your local edits in bar.c into baz.c:

$ svn diff code/baz.c
Index: code/baz.c
===================================================================
--- code/baz.c  (revision 5)
+++ code/baz.c  (working copy)
@@ -1,4 +1,4 @@
 const char *bar(void)
 {
-       return "Me neither!\n";
+       return "Well, I do like being moved around!\n";
 }
[警告] 警告

Local edits to the file bar.c, which is renamed during an update to baz.c, will only be applied to bar.c if your working copy of bar.c is based on the revision in which it was last modified before being moved in the repository. Otherwise, Subversion will resort to retreiving baz.c from the repository, and will not try to transfer your local modifications to it. You will have to do so manually.

svn info shows the URLs of the items involved in the conflict. The left URL shows the source of the local side of the conflict, while the right URL shows the source of the incoming side of the conflict. These URLs indicate where you should start searching the repository's history for the change which conflicts with your local change.

$ svn info code/bar.c | tail -n 4 
Tree conflict: local edit, incoming delete upon update
  Source  left: (file) ^/trunk/code/bar.c@4
  Source right: (none) ^/trunk/code/bar.c@5

bar.c is now said to be the victim of a tree conflict. It cannot be committed until the conflict is resolved:

$ svn commit -m "Small fixes" 
svn: Commit failed (details follow):
svn: Aborting commit: 'code/bar.c' remains in conflict

So how can this conflict be resolved? You can either agree or disagree with the move Harry made. In case you agree, you can delete bar.c and mark the tree conflict as resolved:

$ svn delete --force code/bar.c
D         code/bar.c
$ svn resolve --accept=working code/bar.c
Resolved conflicted state of 'code/bar.c'
$ svn status
M       code/foo.c
M       code/baz.c
$ svn diff
Index: code/foo.c
===================================================================
--- code/foo.c  (revision 5)
+++ code/foo.c  (working copy)
@@ -3,5 +3,5 @@
 int main(int argc, char *argv[])
 {
        printf("I don't like being moved around!\n%s", bar());
-       return 0;
+       return 1;
 }
Index: code/baz.c
===================================================================
--- code/baz.c  (revision 5)
+++ code/baz.c  (working copy)
@@ -1,4 +1,4 @@
 const char *bar(void)
 {
-       return "Me neither!\n";
+       return "Well, I do like being moved around!\n";
 }

If you do not agree with the move, you can delete baz.c instead, after making sure any changes made to it after it was renamed are either preserved or not worth keeping. Do not forget to revert the changes Harry made to the Makefile. Since bar.c is already scheduled for re-addition, there is nothing else left to do, and the conflict can be marked resolved:

$ svn delete --force code/baz.c
D         code/baz.c
$ svn resolve --accept=working code/bar.c
Resolved conflicted state of 'code/bar.c'
$ svn status
M       code/foo.c
A  +    code/bar.c
D       code/baz.c
M       Makefile
$ svn diff
Index: code/foo.c
===================================================================
--- code/foo.c	(revision 5)
+++ code/foo.c	(working copy)
@@ -3,5 +3,5 @@
 int main(int argc, char *argv[])
 {
 	printf("I don't like being moved around!\n%s", bar());
-	return 0;
+	return 1;
 }
Index: code/bar.c
===================================================================
--- code/bar.c	(revision 5)
+++ code/bar.c	(working copy)
@@ -1,4 +1,4 @@
 const char *bar(void)
 {
-	return "Me neither!\n";
+	return "Well, I do like being moved around!\n";
 }
Index: code/baz.c
===================================================================
--- code/baz.c	(revision 5)
+++ code/baz.c	(working copy)
@@ -1,4 +0,0 @@
-const char *bar(void)
-{
-	return "Me neither!\n";
-}
Index: Makefile
===================================================================
--- Makefile	(revision 5)
+++ Makefile	(working copy)
@@ -1,2 +1,2 @@
 foo: 
-	$(CC) -o $@ code/foo.c code/baz.c
+	$(CC) -o $@ code/foo.c code/bar.c

In either case, you have now resolved your first tree conflict! You can commit your changes and tell Harry during tea break about all the extra work he caused for you.



[9] Well, you could mark files containing conflict markers as resolved and commit them, if you really wanted to. But this is rarely done in practice.