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