6320c4fe552d83734ce14c4057ba23d89b0aad3d
[idea/community.git] / plugins / git4idea / tests / git4idea / tests / GitChangeProviderTest.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package git4idea.tests;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.progress.EmptyProgressIndicator;
20 import com.intellij.openapi.util.io.FileUtil;
21 import com.intellij.openapi.vcs.*;
22 import com.intellij.openapi.vcs.changes.Change;
23 import com.intellij.openapi.vcs.changes.ChangeListManager;
24 import com.intellij.openapi.vcs.changes.VcsModifiableDirtyScope;
25 import com.intellij.openapi.vcs.changes.pending.MockChangeListManagerGate;
26 import com.intellij.openapi.vfs.VfsUtil;
27 import com.intellij.openapi.vfs.VirtualFile;
28 import com.intellij.testFramework.vcs.MockChangelistBuilder;
29 import com.intellij.testFramework.vcs.MockDirtyScope;
30 import com.intellij.ui.GuiUtils;
31 import git4idea.GitVcs;
32 import git4idea.changes.GitChangeProvider;
33 import org.testng.annotations.BeforeMethod;
34 import org.testng.annotations.Test;
35
36 import java.util.Arrays;
37 import java.util.HashMap;
38 import java.util.List;
39 import java.util.Map;
40
41 import static com.intellij.openapi.vcs.FileStatus.*;
42 import static org.testng.Assert.*;
43
44 /**
45  * Tests GitChangeProvider functionality. Scenario is the same for all tests:
46  * 1. Modifies files on disk (creates, edits, deletes, etc.)
47  * 2. Manually adds them to a dirty scope.
48  * 3. Calls ChangeProvider.getChanges() and checks that the changes are there.
49  * @author Kirill Likhodedov
50  */
51 public class GitChangeProviderTest extends GitSingleUserTest {
52
53   private GitChangeProvider myChangeProvider;
54   private VcsModifiableDirtyScope myDirtyScope;
55   private Map<String, VirtualFile> myFiles;
56   private VirtualFile afile;
57   private VirtualFile myRootDir;
58
59   @BeforeMethod
60   @Override
61   protected void setUp() throws Exception {
62     super.setUp();
63     myChangeProvider = (GitChangeProvider) GitVcs.getInstance(myProject).getChangeProvider();
64
65     myFiles = GitTestUtil.createFileStructure(myProject, myRepo, "a.txt", "b.txt", "dir/c.txt", "dir/subdir/d.txt");
66     myRepo.addCommit();
67     myRepo.refresh();
68
69     afile = myFiles.get("a.txt"); // the file is commonly used, so save it in a field.
70     myRootDir = myRepo.getDir();
71
72     myDirtyScope = new MockDirtyScope(myProject, GitVcs.getInstance(myProject));
73   }
74
75   @Test
76   public void testCreateFile() throws Exception {
77     VirtualFile file = create(myRootDir, "new.txt");
78     assertChanges(file, ADDED);
79   }
80
81   @Test
82   public void testCreateFileInDir() throws Exception {
83     VirtualFile dir = createDir(myRootDir, "newdir");
84     VirtualFile bfile = create(dir, "new.txt");
85     assertChanges(new VirtualFile[] {bfile, dir}, new FileStatus[] { ADDED, null} );
86   }
87
88   @Test
89   public void testEditFile() throws Exception {
90     edit(afile, "new content");
91     assertChanges(afile, MODIFIED);
92   }
93
94   @Test
95   public void testDeleteFile() throws Exception {
96     delete(afile);
97     assertChanges(afile, DELETED);
98   }
99
100   @Test
101   public void testDeleteDirRecursively() throws Exception {
102     GuiUtils.runOrInvokeAndWait(new Runnable() {
103       @Override
104       public void run() {
105         ApplicationManager.getApplication().runWriteAction(new Runnable() {
106           @Override
107           public void run() {
108             final VirtualFile dir= myRepo.getDir().findChild("dir");
109             myDirtyScope.addDirtyDirRecursively(new FilePathImpl(dir));
110             FileUtil.delete(VfsUtil.virtualToIoFile(dir));
111           }
112         });
113       }
114     });
115     assertChanges(new VirtualFile[] { myFiles.get("dir/c.txt"), myFiles.get("dir/subdir/d.txt") }, new FileStatus[] { DELETED, DELETED });
116   }
117
118   @Test
119   public void testMoveNewFile() throws Exception {
120     // IDEA-59587
121     // Reproducibility of the bug (in the original roots cause) depends on the order of new and old paths in the dirty scope.
122     // MockDirtyScope shouldn't preserve the order of items added there - a Set is returned from getDirtyFiles().
123     // But the order is likely preserved if it meets the natural order of the items inserted into the dirty scope.
124     // That's why the test moves from .../repo/dir/new.txt to .../repo/new.txt - to make the old path appear later than the new one.
125     // This is not consistent though.
126     final VirtualFile dir= myRepo.getDir().findChild("dir");
127     final VirtualFile file = create(dir, "new.txt");
128     move(file, myRootDir);
129     assertChanges(file, ADDED);
130   }
131
132   @Test
133   public void testSimultaneousOperationsOnMultipleFiles() throws Exception {
134     VirtualFile dfile = myFiles.get("dir/subdir/d.txt");
135     VirtualFile cfile = myFiles.get("dir/c.txt");
136
137     edit(afile, "new afile content");
138     edit(cfile, "new cfile content");
139     delete(dfile);
140     VirtualFile newfile = create(myRootDir, "newfile.txt");
141
142     assertChanges(new VirtualFile[] {afile, cfile, dfile, newfile}, new FileStatus[] {MODIFIED, MODIFIED, DELETED, ADDED});
143   }
144
145   /**
146    * "modify-modify" merge conflict.
147    * 1. Create a file and commit it.
148    * 2. Create new branch and switch to it.
149    * 3. Edit the file in that branch and commit.
150    * 4. Switch to master, conflictly edit the file and commit.
151    * 5. Merge the branch on master.
152    * Merge conflict "modify-modify" happens.
153    */
154   @Test
155   public void testConflictMM() throws Exception {
156     modifyFileInBranches("a.txt", FileAction.MODIFY, FileAction.MODIFY);
157     assertChanges(afile, FileStatus.MERGED_WITH_CONFLICTS);
158   }
159
160   /**
161    * Modify-Delete conflict.
162    */
163   @Test
164   public void testConflictMD() throws Exception {
165     modifyFileInBranches("a.txt", FileAction.MODIFY, FileAction.DELETE);
166     assertChanges(afile, FileStatus.MERGED_WITH_CONFLICTS);
167   }
168
169   /**
170    * Delete-Modify conflict.
171    */
172   @Test
173   public void testConflictDM() throws Exception {
174     modifyFileInBranches("a.txt", FileAction.DELETE, FileAction.MODIFY);
175     assertChanges(afile, FileStatus.MERGED_WITH_CONFLICTS);
176   }
177
178   /**
179    * Create a file with conflicting content.
180    */
181   @Test
182   public void testConflictCC() throws Exception {
183     modifyFileInBranches("z.txt", FileAction.CREATE, FileAction.CREATE);
184     VirtualFile zfile = myRepo.getDir().findChild("z.txt");
185     assertChanges(zfile, FileStatus.MERGED_WITH_CONFLICTS);
186   }
187
188   @Test
189   public void testConflictRD() throws Exception {
190     modifyFileInBranches("a.txt", FileAction.RENAME, FileAction.DELETE);
191     VirtualFile newfile = myRepo.getDir().findChild("a.txt_master_new"); // renamed in master
192     assertChanges(newfile, FileStatus.MERGED_WITH_CONFLICTS);
193   }
194
195   @Test
196   public void testConflictDR() throws Exception {
197     modifyFileInBranches("a.txt", FileAction.DELETE, FileAction.RENAME);
198     VirtualFile newFile = myRepo.getDir().findChild("a.txt_feature_new"); // deleted in master, renamed in feature
199     assertChanges(newFile, FileStatus.MERGED_WITH_CONFLICTS);
200   }
201
202   private void modifyFileInBranches(String filename, FileAction masterAction, FileAction featureAction) throws Exception {
203     myRepo.createBranch("feature");
204     performActionOnFileAndRecordToIndex(filename, "feature", featureAction);
205     myRepo.commit();
206     myRepo.checkout("master");
207     performActionOnFileAndRecordToIndex(filename, "master", masterAction);
208     myRepo.commit();
209     myRepo.merge("feature");
210     myRepo.refresh();
211   }
212
213   private enum FileAction {
214     CREATE, MODIFY, DELETE, RENAME
215   }
216
217   private void performActionOnFileAndRecordToIndex(String filename, String branchName, FileAction action) throws Exception {
218     VirtualFile file = myRepo.getDir().findChild(filename);
219     switch (action) {
220       case CREATE:
221         final VirtualFile createdFile = createFileInCommand(filename, "initial content in branch " + branchName);
222         dirty(createdFile);
223         myRepo.add(filename);
224         break;
225       case MODIFY:
226         editFileInCommand(file, "new content in branch " + branchName);
227         dirty(file);
228         myRepo.add(filename);
229         break;
230       case DELETE:
231         dirty(file);
232         myRepo.rm(filename);
233         break;
234       case RENAME:
235         String newName = filename + "_" + branchName.replaceAll("\\s", "_") + "_new";
236         dirty(file);
237         myRepo.mv(filename, newName);
238         dirty(myRootDir.findChild(newName));
239         break;
240       default:
241         break;
242     }
243   }
244
245   /**
246    * Checks that the given files have respective statuses in the change list retrieved from myChangesProvider.
247    * Pass null in the fileStatuses array to indicate that proper file has not changed.
248    */
249   private void assertChanges(VirtualFile[] virtualFiles, FileStatus[] fileStatuses) throws VcsException {
250     Map<FilePath, Change> result = getChanges(virtualFiles);
251     for (int i = 0; i < virtualFiles.length; i++) {
252       FilePath fp = new FilePathImpl(virtualFiles[i]);
253       FileStatus status = fileStatuses[i];
254       if (status == null) {
255         assertFalse(result.containsKey(fp), "File [" + tos(fp) + " shouldn't be in the change list, but it was.");
256         continue;
257       }
258       assertTrue(result.containsKey(fp), "File [" + tos(fp) + "] didn't change. Changes: " + tos(result));
259       assertEquals(result.get(fp).getFileStatus(), status, "File statuses don't match for file [" + tos(fp) + "]");
260     }
261   }
262
263   private void assertChanges(VirtualFile virtualFile, FileStatus fileStatus) throws VcsException {
264     assertChanges(new VirtualFile[] { virtualFile }, new FileStatus[] { fileStatus });
265   }
266
267   /**
268    * Marks the given files dirty in myDirtyScope, gets changes from myChangeProvider and groups the changes in the map.
269    * Assumes that only one change for a file has happened.
270    */
271   private Map<FilePath, Change> getChanges(VirtualFile... changedFiles) throws VcsException {
272     final List<FilePath> changedPaths = ObjectsConvertor.vf2fp(Arrays.asList(changedFiles));
273
274     // get changes
275     MockChangelistBuilder builder = new MockChangelistBuilder();
276     myChangeProvider.getChanges(myDirtyScope, builder, new EmptyProgressIndicator(), new MockChangeListManagerGate(ChangeListManager.getInstance(myProject)));
277     List<Change> changes = builder.getChanges();
278
279     // get changes for files
280     final Map<FilePath, Change> result = new HashMap<FilePath, Change>();
281     for (Change change : changes) {
282       VirtualFile file = change.getVirtualFile();
283       FilePath filePath = null;
284       if (file == null) { // if a file was deleted, just find the reference in the original list of files and use it. 
285         String path = change.getBeforeRevision().getFile().getPath();
286         for (FilePath fp : changedPaths) {
287           if (fp.getPath().equals(path)) {
288             filePath = fp;
289             break;
290           }
291         }
292       } else {
293         filePath = new FilePathImpl(file);
294       }
295       result.put(filePath, change);
296     }
297     return result;
298   }
299
300   private VirtualFile create(VirtualFile parent, String name) {
301     return create(parent, name, false);
302   }
303
304   private VirtualFile createDir(VirtualFile parent, String name) {
305     return create(parent, name, true);
306   }
307
308   private VirtualFile create(VirtualFile parent, String name, boolean dir) {
309     final VirtualFile file = dir ? createDirInCommand(parent, name) : createFileInCommand(parent, name, "content" + Math.random());
310     dirty(file);
311     return file;
312   }
313
314   private void edit(VirtualFile file, String content) {
315     editFileInCommand(file, content);
316     dirty(file);
317   }
318
319   private void move(VirtualFile file, VirtualFile newParent) {
320     dirty(file);
321     moveFileInCommand(file, newParent);
322     dirty(file);
323   }
324
325   private void delete(VirtualFile file) {
326     dirty(file);
327     deleteFileInCommand(file);
328   }
329
330   private void dirty(VirtualFile file) {
331     myDirtyScope.addDirtyFile(new FilePathImpl(file));
332   }
333 }