d4fe1491df809e01baf9f550a83fc29152580248
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / actions / diff / ChangeDiffRequestProducer.java
1 /*
2  * Copyright 2000-2013 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 com.intellij.openapi.vcs.changes.actions.diff;
17
18 import com.intellij.diff.DiffContentFactory;
19 import com.intellij.diff.DiffContentFactoryEx;
20 import com.intellij.diff.DiffRequestFactory;
21 import com.intellij.diff.DiffRequestFactoryImpl;
22 import com.intellij.diff.chains.DiffRequestProducer;
23 import com.intellij.diff.chains.DiffRequestProducerException;
24 import com.intellij.diff.contents.DiffContent;
25 import com.intellij.diff.impl.DiffViewerWrapper;
26 import com.intellij.diff.merge.MergeUtil;
27 import com.intellij.diff.requests.DiffRequest;
28 import com.intellij.diff.requests.ErrorDiffRequest;
29 import com.intellij.diff.requests.SimpleDiffRequest;
30 import com.intellij.diff.util.DiffUserDataKeys;
31 import com.intellij.diff.util.DiffUserDataKeysEx;
32 import com.intellij.diff.util.DiffUtil;
33 import com.intellij.diff.util.Side;
34 import com.intellij.openapi.diagnostic.Logger;
35 import com.intellij.openapi.progress.ProcessCanceledException;
36 import com.intellij.openapi.progress.ProgressIndicator;
37 import com.intellij.openapi.project.Project;
38 import com.intellij.openapi.util.Comparing;
39 import com.intellij.openapi.util.Key;
40 import com.intellij.openapi.util.Ref;
41 import com.intellij.openapi.util.UserDataHolder;
42 import com.intellij.openapi.vcs.AbstractVcs;
43 import com.intellij.openapi.vcs.FilePath;
44 import com.intellij.openapi.vcs.VcsDataKeys;
45 import com.intellij.openapi.vcs.VcsException;
46 import com.intellij.openapi.vcs.changes.*;
47 import com.intellij.openapi.vcs.merge.MergeData;
48 import com.intellij.openapi.vfs.LocalFileSystem;
49 import com.intellij.openapi.vfs.VirtualFile;
50 import com.intellij.util.ThreeState;
51 import com.intellij.util.containers.ContainerUtil;
52 import com.intellij.util.ui.UIUtil;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
55
56 import java.io.IOException;
57 import java.util.Collections;
58 import java.util.List;
59 import java.util.Map;
60
61 public class ChangeDiffRequestProducer implements DiffRequestProducer {
62   private static final Logger LOG = Logger.getInstance(ChangeDiffRequestProducer.class);
63
64   public static Key<Change> CHANGE_KEY = Key.create("DiffRequestPresentable.Change");
65
66   @Nullable private final Project myProject;
67   @NotNull private final Change myChange;
68   @NotNull private final Map<Key, Object> myChangeContext;
69
70   private ChangeDiffRequestProducer(@Nullable Project project, @NotNull Change change, @NotNull Map<Key, Object> changeContext) {
71     myChange = change;
72     myProject = project;
73     myChangeContext = changeContext;
74   }
75
76   @NotNull
77   public Change getChange() {
78     return myChange;
79   }
80
81   @Nullable
82   public Project getProject() {
83     return myProject;
84   }
85
86   @NotNull
87   @Override
88   public String getName() {
89     return ChangesUtil.getFilePath(myChange).getPath();
90   }
91
92   public static boolean isEquals(@NotNull Change change1, @NotNull Change change2) {
93     for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) {
94       ThreeState equals = provider.isEquals(change1, change2);
95       if (equals == ThreeState.NO) return false;
96     }
97     for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) {
98       ThreeState equals = provider.isEquals(change1, change2);
99       if (equals == ThreeState.YES) return true;
100       if (equals == ThreeState.NO) return false;
101     }
102
103     if (!Comparing.equal(change1.getClass(), change2.getClass())) return false;
104     if (!isEquals(change1.getBeforeRevision(), change2.getBeforeRevision())) return false;
105     if (!isEquals(change1.getAfterRevision(), change2.getAfterRevision())) return false;
106
107     return true;
108   }
109
110   private static boolean isEquals(@Nullable ContentRevision revision1, @Nullable ContentRevision revision2) {
111     if (Comparing.equal(revision1, revision2)) return true;
112     if (revision1 instanceof CurrentContentRevision && revision2 instanceof CurrentContentRevision) {
113       VirtualFile vFile1 = ((CurrentContentRevision)revision1).getVirtualFile();
114       VirtualFile vFile2 = ((CurrentContentRevision)revision2).getVirtualFile();
115       return Comparing.equal(vFile1, vFile2);
116     }
117     return false;
118   }
119
120   @Nullable
121   public static ChangeDiffRequestProducer create(@Nullable Project project, @NotNull Change change) {
122     return create(project, change, Collections.<Key, Object>emptyMap());
123   }
124
125   @Nullable
126   public static ChangeDiffRequestProducer create(@Nullable Project project,
127                                                  @NotNull Change change,
128                                                  @NotNull Map<Key, Object> changeContext) {
129     if (!canCreate(project, change)) return null;
130     return new ChangeDiffRequestProducer(project, change, changeContext);
131   }
132
133   public static boolean canCreate(@Nullable Project project, @NotNull Change change) {
134     for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) {
135       if (provider.canCreate(project, change)) return true;
136     }
137     for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) {
138       if (provider.canCreate(project, change)) return true;
139     }
140
141     ContentRevision bRev = change.getBeforeRevision();
142     ContentRevision aRev = change.getAfterRevision();
143
144     if (bRev == null && aRev == null) return false;
145     if (bRev != null && bRev.getFile().isDirectory()) return false;
146     if (aRev != null && aRev.getFile().isDirectory()) return false;
147
148     return true;
149   }
150
151   @NotNull
152   @Override
153   public DiffRequest process(@NotNull UserDataHolder context,
154                              @NotNull ProgressIndicator indicator) throws DiffRequestProducerException, ProcessCanceledException {
155     try {
156       return loadCurrentContents(context, indicator);
157     }
158     catch (ProcessCanceledException e) {
159       throw e;
160     }
161     catch (DiffRequestProducerException e) {
162       throw e;
163     }
164     catch (Exception e) {
165       LOG.warn(e);
166       throw new DiffRequestProducerException(e.getMessage());
167     }
168   }
169
170   @NotNull
171   protected DiffRequest loadCurrentContents(@NotNull UserDataHolder context,
172                                             @NotNull ProgressIndicator indicator) throws DiffRequestProducerException {
173     DiffRequestProducerException wrapperException = null;
174     DiffRequestProducerException requestException = null;
175
176     DiffViewerWrapper wrapper = null;
177     try {
178       for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) {
179         if (provider.canCreate(myProject, myChange)) {
180           wrapper = provider.process(this, context, indicator);
181           break;
182         }
183       }
184     }
185     catch (DiffRequestProducerException e) {
186       wrapperException = e;
187     }
188
189     DiffRequest request = null;
190     try {
191       for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) {
192         if (provider.canCreate(myProject, myChange)) {
193           request = provider.process(this, context, indicator);
194           break;
195         }
196       }
197       if (request == null) request = createRequest(myProject, myChange, context, indicator);
198     }
199     catch (DiffRequestProducerException e) {
200       requestException = e;
201     }
202
203     if (requestException != null && wrapperException != null) {
204       String message = requestException.getMessage() + "\n\n" + wrapperException.getMessage();
205       throw new DiffRequestProducerException(message);
206     }
207     if (requestException != null) {
208       request = new ErrorDiffRequest(getRequestTitle(myChange), requestException);
209       LOG.info("Request: " + requestException.getMessage());
210     }
211     if (wrapperException != null) {
212       LOG.info("Wrapper: " + wrapperException.getMessage());
213     }
214
215     request.putUserData(CHANGE_KEY, myChange);
216     request.putUserData(DiffViewerWrapper.KEY, wrapper);
217
218     for (Map.Entry<Key, Object> entry : myChangeContext.entrySet()) {
219       request.putUserData(entry.getKey(), entry.getValue());
220     }
221
222     DiffUtil.putDataKey(request, VcsDataKeys.CURRENT_CHANGE, myChange);
223
224     return request;
225   }
226
227   @NotNull
228   private DiffRequest createRequest(@Nullable Project project,
229                                            @NotNull Change change,
230                                            @NotNull UserDataHolder context,
231                                            @NotNull ProgressIndicator indicator) throws DiffRequestProducerException {
232     if (ChangesUtil.isTextConflictingChange(change)) { // three side diff
233       // FIXME: This part is ugly as a VCS merge subsystem itself.
234
235       FilePath path = ChangesUtil.getFilePath(change);
236       VirtualFile file = path.getVirtualFile();
237       if (file == null) {
238         file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.getPath());
239       }
240       if (file == null) throw new DiffRequestProducerException("Can't show merge conflict - file not found");
241
242       if (project == null) {
243         throw new DiffRequestProducerException("Can't show merge conflict - project is unknown");
244       }
245       final AbstractVcs vcs = ChangesUtil.getVcsForChange(change, project);
246       if (vcs == null || vcs.getMergeProvider() == null) {
247         throw new DiffRequestProducerException("Can't show merge conflict - operation nos supported");
248       }
249       try {
250         // FIXME: loadRevisions() can call runProcessWithProgressSynchronously() inside
251         final Ref<Throwable> exceptionRef = new Ref<>();
252         final Ref<MergeData> mergeDataRef = new Ref<>();
253         final VirtualFile finalFile = file;
254         UIUtil.invokeAndWaitIfNeeded(new Runnable() {
255           @Override
256           public void run() {
257             try {
258               mergeDataRef.set(vcs.getMergeProvider().loadRevisions(finalFile));
259             }
260             catch (VcsException e) {
261               exceptionRef.set(e);
262             }
263           }
264         });
265         if (!exceptionRef.isNull()) {
266           Throwable e = exceptionRef.get();
267           if (e instanceof VcsException) throw (VcsException)e;
268           if (e instanceof Error) throw (Error)e;
269           if (e instanceof RuntimeException) throw (RuntimeException)e;
270           throw new RuntimeException(e);
271         }
272         MergeData mergeData = mergeDataRef.get();
273
274         ContentRevision bRev = change.getBeforeRevision();
275         ContentRevision aRev = change.getAfterRevision();
276         String beforeRevisionTitle = getRevisionTitle(bRev, "Your version");
277         String afterRevisionTitle = getRevisionTitle(aRev, "Server version");
278
279         String title = DiffRequestFactory.getInstance().getTitle(file);
280         List<String> titles = ContainerUtil.list(beforeRevisionTitle, "Base Version", afterRevisionTitle);
281
282         DiffContentFactory contentFactory = DiffContentFactory.getInstance();
283         List<DiffContent> contents = ContainerUtil.list(
284           contentFactory.createFromBytes(project, mergeData.CURRENT, file),
285           contentFactory.createFromBytes(project, mergeData.ORIGINAL, file),
286           contentFactory.createFromBytes(project, mergeData.LAST, file)
287         );
288
289         SimpleDiffRequest request = new SimpleDiffRequest(title, contents, titles);
290         MergeUtil.putRevisionInfos(request, mergeData);
291
292         return request;
293       }
294       catch (VcsException e) {
295         LOG.info(e);
296         throw new DiffRequestProducerException(e);
297       }
298       catch (IOException e) {
299         LOG.info(e);
300         throw new DiffRequestProducerException(e);
301       }
302     }
303     else {
304       ContentRevision bRev = change.getBeforeRevision();
305       ContentRevision aRev = change.getAfterRevision();
306
307       if (bRev == null && aRev == null) {
308         LOG.warn("Both revision contents are empty");
309         throw new DiffRequestProducerException("Bad revisions contents");
310       }
311       if (bRev != null) checkContentRevision(project, bRev, context, indicator);
312       if (aRev != null) checkContentRevision(project, aRev, context, indicator);
313
314       String title = getRequestTitle(change);
315
316       indicator.setIndeterminate(true);
317       DiffContent content1 = createContent(project, bRev, context, indicator);
318       DiffContent content2 = createContent(project, aRev, context, indicator);
319
320       final String userLeftRevisionTitle = (String)myChangeContext.get(DiffUserDataKeysEx.VCS_DIFF_LEFT_CONTENT_TITLE);
321       String beforeRevisionTitle = userLeftRevisionTitle != null ? userLeftRevisionTitle : getRevisionTitle(bRev, "Base version");
322       final String userRightRevisionTitle = (String)myChangeContext.get(DiffUserDataKeysEx.VCS_DIFF_RIGHT_CONTENT_TITLE);
323       String afterRevisionTitle = userRightRevisionTitle != null ? userRightRevisionTitle : getRevisionTitle(aRev, "Your version");
324
325       SimpleDiffRequest request = new SimpleDiffRequest(title, content1, content2, beforeRevisionTitle, afterRevisionTitle);
326
327       boolean bRevCurrent = bRev instanceof CurrentContentRevision;
328       boolean aRevCurrent = aRev instanceof CurrentContentRevision;
329       if (bRevCurrent && !aRevCurrent) request.putUserData(DiffUserDataKeys.MASTER_SIDE, Side.LEFT);
330       if (!bRevCurrent && aRevCurrent) request.putUserData(DiffUserDataKeys.MASTER_SIDE, Side.RIGHT);
331
332       return request;
333     }
334   }
335
336   @NotNull
337   public static String getRequestTitle(@NotNull Change change) {
338     ContentRevision bRev = change.getBeforeRevision();
339     ContentRevision aRev = change.getAfterRevision();
340
341     assert bRev != null || aRev != null;
342     if (bRev != null && aRev != null) {
343       FilePath bPath = bRev.getFile();
344       FilePath aPath = aRev.getFile();
345       if (bPath.equals(aPath)) {
346         return DiffRequestFactoryImpl.getContentTitle(bPath);
347       }
348       else {
349         return DiffRequestFactoryImpl.getTitle(bPath, aPath, " -> ");
350       }
351     }
352     else if (bRev != null) {
353       return DiffRequestFactoryImpl.getContentTitle(bRev.getFile());
354     }
355     else {
356       return DiffRequestFactoryImpl.getContentTitle(aRev.getFile());
357     }
358   }
359
360   @NotNull
361   public static String getRevisionTitle(@Nullable ContentRevision revision, @NotNull String defaultValue) {
362     if (revision == null) return defaultValue;
363     String title = revision.getRevisionNumber().asString();
364     if (title == null || title.isEmpty()) return defaultValue;
365     return title;
366   }
367
368   @NotNull
369   public static DiffContent createContent(@Nullable Project project,
370                                           @Nullable ContentRevision revision,
371                                           @NotNull UserDataHolder context,
372                                           @NotNull ProgressIndicator indicator) throws DiffRequestProducerException {
373     try {
374       indicator.checkCanceled();
375
376       if (revision == null) return DiffContentFactory.getInstance().createEmpty();
377       FilePath filePath = revision.getFile();
378       DiffContentFactoryEx contentFactory = DiffContentFactoryEx.getInstanceEx();
379
380       if (revision instanceof CurrentContentRevision) {
381         VirtualFile vFile = ((CurrentContentRevision)revision).getVirtualFile();
382         if (vFile == null) throw new DiffRequestProducerException("Can't get current revision content");
383         return contentFactory.create(project, vFile);
384       }
385
386       if (revision instanceof BinaryContentRevision) {
387         byte[] content = ((BinaryContentRevision)revision).getBinaryContent();
388         if (content == null) {
389           throw new DiffRequestProducerException("Can't get binary revision content");
390         }
391         return contentFactory.createBinary(project, content, filePath.getFileType(), filePath.getName());
392       }
393
394       if (revision instanceof ByteBackedContentRevision) {
395         byte[] revisionContent = ((ByteBackedContentRevision)revision).getContentAsBytes();
396         if (revisionContent == null) throw new DiffRequestProducerException("Can't get revision content");
397         return contentFactory.createFromBytes(project, revisionContent, filePath);
398       }
399       else {
400         String revisionContent = revision.getContent();
401         if (revisionContent == null) throw new DiffRequestProducerException("Can't get revision content");
402         return contentFactory.create(project, revisionContent, filePath);
403       }
404     }
405     catch (IOException e) {
406       LOG.info(e);
407       throw new DiffRequestProducerException(e);
408     }
409     catch (VcsException e) {
410       LOG.info(e);
411       throw new DiffRequestProducerException(e);
412     }
413   }
414
415   public static void checkContentRevision(@Nullable Project project,
416                                           @NotNull ContentRevision rev,
417                                           @NotNull UserDataHolder context,
418                                           @NotNull ProgressIndicator indicator) throws DiffRequestProducerException {
419     if (rev.getFile().isDirectory()) {
420       throw new DiffRequestProducerException("Can't show diff for directory");
421     }
422   }
423
424   @Override
425   public boolean equals(Object o) {
426     if (this == o) return true;
427     if (o == null || getClass() != o.getClass()) return false;
428
429     ChangeDiffRequestProducer that = (ChangeDiffRequestProducer)o;
430
431     return myChange.equals(that.myChange);
432   }
433
434   @Override
435   public int hashCode() {
436     return myChange.hashCode();
437   }
438 }