87d45cd10486a4d92752d82a4373b5881e833d37
[idea/community.git] / plugins / hg4idea / src / org / zmlx / hg4idea / provider / HgChangeProvider.java
1 // Copyright 2008-2010 Victor Iacoban
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software distributed under
10 // the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 // either express or implied. See the License for the specific language governing permissions and
12 // limitations under the License.
13 package org.zmlx.hg4idea.provider;
14
15 import com.intellij.openapi.components.ServiceManager;
16 import com.intellij.openapi.fileEditor.FileDocumentManager;
17 import com.intellij.openapi.progress.ProgressIndicator;
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.vcs.*;
20 import com.intellij.openapi.vcs.changes.*;
21 import com.intellij.openapi.vfs.VirtualFile;
22 import com.intellij.vcsUtil.VcsUtil;
23 import org.zmlx.hg4idea.*;
24 import org.zmlx.hg4idea.command.*;
25
26 import java.awt.*;
27 import java.util.*;
28 import java.util.List;
29
30 public class HgChangeProvider implements ChangeProvider {
31
32   private final Project myProject;
33   private final VcsKey myVcsKey;
34
35   public static final FileStatus COPIED =
36     ServiceManager.getService(FileStatusFactory.class).createFileStatus("COPIED", "Copied", FileStatus.COLOR_ADDED);
37   public static final FileStatus RENAMED = ServiceManager.getService(FileStatusFactory.class).createFileStatus("RENAMED", "Renamed",
38                                                                                                                Color.cyan.darker().darker());
39
40   private static final EnumMap<HgFileStatusEnum, HgChangeProcessor> PROCESSORS =
41     new EnumMap<HgFileStatusEnum, HgChangeProcessor>(HgFileStatusEnum.class);
42
43   static {
44     PROCESSORS.put(HgFileStatusEnum.ADDED, HgChangeProcessor.ADDED);
45     PROCESSORS.put(HgFileStatusEnum.DELETED, HgChangeProcessor.DELETED);
46     PROCESSORS.put(HgFileStatusEnum.IGNORED, HgChangeProcessor.IGNORED);
47     PROCESSORS.put(HgFileStatusEnum.MISSING, HgChangeProcessor.MISSING);
48     PROCESSORS.put(HgFileStatusEnum.COPY, HgChangeProcessor.COPIED);
49     PROCESSORS.put(HgFileStatusEnum.MODIFIED, HgChangeProcessor.MODIFIED);
50     PROCESSORS.put(HgFileStatusEnum.UNMODIFIED, HgChangeProcessor.UNMODIFIED);
51     PROCESSORS.put(HgFileStatusEnum.UNVERSIONED, HgChangeProcessor.UNVERSIONED);
52   }
53
54   public HgChangeProvider(Project project, VcsKey vcsKey) {
55     myProject = project;
56     myVcsKey = vcsKey;
57   }
58
59   public void getChanges(VcsDirtyScope dirtyScope, ChangelistBuilder builder,
60                          ProgressIndicator progress, ChangeListManagerGate addGate) throws VcsException {
61     final Collection<HgChange> changes = new HashSet<HgChange>();
62
63     for (FilePath filePath : dirtyScope.getRecursivelyDirtyDirectories()) {
64       changes.addAll(process(builder, filePath));
65     }
66     for (FilePath filePath : dirtyScope.getDirtyFiles()) {
67       changes.addAll(process(builder, filePath));
68     }
69
70     processUnsavedChanges(builder, dirtyScope.getDirtyFilesNoExpand(), changes);
71   }
72
73   public boolean isModifiedDocumentTrackingRequired() {
74     return true;
75   }
76
77   public void doCleanup(List<VirtualFile> files) {
78   }
79
80   private Collection<HgChange> process(ChangelistBuilder builder,
81     FilePath filePath) {
82     VirtualFile repo = VcsUtil.getVcsRootFor(myProject, filePath);
83     if (repo == null) {
84       return new HashSet<HgChange>();
85     }
86     Set<HgChange> hgChanges = new HgStatusCommand(myProject).execute(repo, filePath.getPath());
87     if (hgChanges == null || hgChanges.isEmpty()) {
88       return new HashSet<HgChange>();
89     }
90     sendChanges(builder, hgChanges,
91       new HgResolveCommand(myProject).getListSynchronously(repo),
92       new HgWorkingCopyRevisionsCommand(myProject).identify(repo),
93       new HgWorkingCopyRevisionsCommand(myProject).firstParent(repo)
94     );
95     return hgChanges;
96   }
97
98   private void sendChanges(ChangelistBuilder builder, Set<HgChange> changes,
99     Map<HgFile, HgResolveStatusEnum> resolveStatus, HgRevisionNumber workingRevision,
100     HgRevisionNumber parentRevision) {
101     for (HgChange change : changes) {
102       HgFile afterFile = change.afterFile();
103       HgFile beforeFile = change.beforeFile();
104       HgFileStatusEnum status = change.getStatus();
105
106       if (resolveStatus.containsKey(afterFile)
107         && resolveStatus.get(afterFile) == HgResolveStatusEnum.UNRESOLVED) {
108         builder.processChange(
109           new Change(
110             new HgContentRevision(myProject, beforeFile, parentRevision),
111             HgCurrentContentRevision.create(afterFile, workingRevision),
112             FileStatus.MERGED_WITH_CONFLICTS
113           ), myVcsKey);
114         continue;
115       }
116
117       if (isDeleteOfCopiedFile(change, changes)) {
118         // Don't register the 'delete' change for renamed or moved files; IDEA already handles these
119         // itself.
120         continue;
121       }
122
123       HgChangeProcessor processor = PROCESSORS.get(status);
124       if (processor != null) {
125         processor.process(myProject, myVcsKey, builder,
126           workingRevision, parentRevision, beforeFile, afterFile);
127       }
128     }
129   }
130
131   private static boolean isDeleteOfCopiedFile(HgChange change, Set<HgChange> changes) {
132     if (change.getStatus().equals(HgFileStatusEnum.DELETED)) {
133       for (HgChange otherChange : changes) {
134         if (otherChange.getStatus().equals(HgFileStatusEnum.COPY) &&
135           otherChange.beforeFile().equals(change.afterFile())) {
136           return true;
137         }
138       }
139     }
140
141     return false;
142   }
143
144   /**
145    * Finds modified but unsaved files in the given list of dirty files and notifies the builder about MODIFIED changes.
146    * Changes contained in <code>alreadyProcessed</code> are skipped - they have already been processed as modified, or else.
147    */
148   public void processUnsavedChanges(ChangelistBuilder builder, Set<FilePath> dirtyFiles, Collection<HgChange> alreadyProcessed) {
149     // exclude already processed
150     for (HgChange c : alreadyProcessed) {
151       dirtyFiles.remove(c.beforeFile().toFilePath());
152       dirtyFiles.remove(c.afterFile().toFilePath());
153     }
154
155     final ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject);
156     final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
157     for (FilePath filePath : dirtyFiles) {
158       final VirtualFile vf = filePath.getVirtualFile();
159       if (vf != null &&  fileDocumentManager.isFileModified(vf)) {
160         final VirtualFile root = vcsManager.getVcsRootFor(vf);
161         if (root != null) {
162           final HgRevisionNumber beforeRevisionNumber = new HgWorkingCopyRevisionsCommand(myProject).tip(root);
163           final ContentRevision beforeRevision = (beforeRevisionNumber == null ? null : 
164                                                   new HgContentRevision(myProject, new HgFile(myProject, vf), beforeRevisionNumber));
165           builder.processChange(new Change(beforeRevision, CurrentContentRevision.create(filePath), FileStatus.MODIFIED), myVcsKey);
166         }
167       }
168     }
169   }
170
171
172   private enum HgChangeProcessor {
173     ADDED() {
174       @Override
175       void process(Project project, VcsKey vcsKey, ChangelistBuilder builder,
176         HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
177         HgFile beforeFile, HgFile afterFile) {
178         processChange(
179           null,
180           HgCurrentContentRevision.create(afterFile, currentNumber),
181           FileStatus.ADDED,
182           builder,
183           vcsKey
184         );
185       }
186     },
187
188     DELETED() {
189       @Override
190       void process(Project project, VcsKey vcsKey, ChangelistBuilder builder,
191         HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
192         HgFile beforeFile, HgFile afterFile) {
193         processChange(
194           new HgContentRevision(project, beforeFile, parentRevision),
195           null,
196           FileStatus.DELETED,
197           builder,
198           vcsKey
199         );
200       }
201     },
202
203     IGNORED() {
204       @Override
205       void process(Project project, VcsKey vcsKey, ChangelistBuilder builder,
206         HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
207         HgFile beforeFile, HgFile afterFile) {
208         builder.processIgnoredFile(VcsUtil.getVirtualFile(afterFile.getFile()));
209       }
210     },
211
212     MISSING() {
213       @Override
214       void process(Project project, VcsKey vcsKey, ChangelistBuilder builder,
215                    HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
216                    HgFile beforeFile, HgFile afterFile) {
217         builder.processLocallyDeletedFile(new LocallyDeletedChange(beforeFile.toFilePath()));
218       }
219     },
220
221     COPIED() {
222       @Override
223       void process(Project project, VcsKey vcsKey, ChangelistBuilder builder,
224         HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
225         HgFile beforeFile, HgFile afterFile) {
226         if (beforeFile.getFile().exists()) {
227           // The original file exists so this is a duplication of the file.
228           // Don't create the before ContentRevision or IDEA will think
229           // this was a rename.
230           processChange(
231             null,
232             HgCurrentContentRevision.create(afterFile, currentNumber),
233             HgChangeProvider.COPIED,
234             builder,
235             vcsKey
236           );
237         } else {
238           // The original file does not exists so this is a rename.
239           processChange(
240             new HgContentRevision(project, beforeFile, parentRevision),
241             HgCurrentContentRevision.create(afterFile, currentNumber),
242             HgChangeProvider.RENAMED,
243             builder,
244             vcsKey
245           );
246         }
247       }
248     },
249
250     MODIFIED() {
251       @Override
252       void process(Project project, VcsKey vcsKey, ChangelistBuilder builder,
253         HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
254         HgFile beforeFile, HgFile afterFile) {
255         processChange(
256           new HgContentRevision(project, beforeFile, parentRevision),
257           HgCurrentContentRevision.create(afterFile, currentNumber),
258           FileStatus.MODIFIED,
259           builder,
260           vcsKey
261         );
262       }
263     },
264
265     UNMODIFIED() {
266       @Override
267       void process(Project project, VcsKey vcsKey, ChangelistBuilder builder,
268         HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
269         HgFile beforeFile, HgFile afterFile) {
270         //DO NOTHING
271       }
272     },
273
274     UNVERSIONED() {
275       @Override
276       void process(Project project, VcsKey vcsKey, ChangelistBuilder builder,
277         HgRevisionNumber currentNumber, HgRevisionNumber parentRevision,
278         HgFile beforeFile, HgFile afterFile) {
279         builder.processUnversionedFile(VcsUtil.getVirtualFile(afterFile.getFile()));
280       }
281     };
282
283     abstract void process(
284       Project project,
285       VcsKey vcsKey,
286       ChangelistBuilder builder,
287       HgRevisionNumber currentNumber,
288       HgRevisionNumber parentRevision,
289       HgFile beforeFile,
290       HgFile afterFile
291     );
292
293     final void processChange(ContentRevision contentRevisionBefore,
294       ContentRevision contentRevisionAfter, FileStatus fileStatus,
295       ChangelistBuilder builder, VcsKey vcsKey) {
296       if (contentRevisionBefore == null && contentRevisionAfter == null) {
297         return;
298       }
299       builder.processChange(
300         new Change(contentRevisionBefore, contentRevisionAfter, fileStatus),
301         vcsKey
302       );
303     }
304   }
305 }