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