/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.vcs.changes.actions.diff; import com.intellij.diff.DiffContentFactory; import com.intellij.diff.DiffContentFactoryEx; import com.intellij.diff.DiffRequestFactory; import com.intellij.diff.DiffRequestFactoryImpl; import com.intellij.diff.chains.DiffRequestProducer; import com.intellij.diff.chains.DiffRequestProducerException; import com.intellij.diff.contents.DiffContent; import com.intellij.diff.impl.DiffViewerWrapper; import com.intellij.diff.merge.MergeUtil; import com.intellij.diff.requests.DiffRequest; import com.intellij.diff.requests.ErrorDiffRequest; import com.intellij.diff.requests.SimpleDiffRequest; import com.intellij.diff.util.DiffUserDataKeys; import com.intellij.diff.util.DiffUserDataKeysEx; import com.intellij.diff.util.DiffUtil; import com.intellij.diff.util.Side; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.UserDataHolder; import com.intellij.openapi.vcs.AbstractVcs; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.VcsDataKeys; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vcs.merge.MergeData; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ThreeState; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; public class ChangeDiffRequestProducer implements DiffRequestProducer { private static final Logger LOG = Logger.getInstance(ChangeDiffRequestProducer.class); public static Key<Change> CHANGE_KEY = Key.create("DiffRequestPresentable.Change"); @Nullable private final Project myProject; @NotNull private final Change myChange; @NotNull private final Map<Key, Object> myChangeContext; private ChangeDiffRequestProducer(@Nullable Project project, @NotNull Change change, @NotNull Map<Key, Object> changeContext) { myChange = change; myProject = project; myChangeContext = changeContext; } @NotNull public Change getChange() { return myChange; } @Nullable public Project getProject() { return myProject; } @NotNull @Override public String getName() { return ChangesUtil.getFilePath(myChange).getPath(); } public static boolean isEquals(@NotNull Change change1, @NotNull Change change2) { if (!Comparing.equal(ChangesUtil.getBeforePath(change1), ChangesUtil.getBeforePath(change2)) || !Comparing.equal(ChangesUtil.getAfterPath(change1), ChangesUtil.getAfterPath(change2))) { // we use file paths for hashCode, so removing this check might violate comparison contract return false; } for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) { ThreeState equals = provider.isEquals(change1, change2); if (equals == ThreeState.NO) return false; } for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) { ThreeState equals = provider.isEquals(change1, change2); if (equals == ThreeState.YES) return true; if (equals == ThreeState.NO) return false; } if (!Comparing.equal(change1.getClass(), change2.getClass())) return false; if (!Comparing.equal(change1.getFileStatus(), change2.getFileStatus())) return false; if (!isEquals(change1.getBeforeRevision(), change2.getBeforeRevision())) return false; if (!isEquals(change1.getAfterRevision(), change2.getAfterRevision())) return false; return true; } private static boolean isEquals(@Nullable ContentRevision revision1, @Nullable ContentRevision revision2) { if (Comparing.equal(revision1, revision2)) return true; if (revision1 instanceof CurrentContentRevision && revision2 instanceof CurrentContentRevision) { VirtualFile vFile1 = ((CurrentContentRevision)revision1).getVirtualFile(); VirtualFile vFile2 = ((CurrentContentRevision)revision2).getVirtualFile(); return Comparing.equal(vFile1, vFile2); } return false; } public static int hashCode(@NotNull Change change) { return hashCode(change.getBeforeRevision()) + 31 * hashCode(change.getAfterRevision()); } private static int hashCode(@Nullable ContentRevision revision) { return revision != null ? revision.getFile().hashCode() : 0; } @Nullable public static ChangeDiffRequestProducer create(@Nullable Project project, @NotNull Change change) { return create(project, change, Collections.emptyMap()); } @Nullable public static ChangeDiffRequestProducer create(@Nullable Project project, @NotNull Change change, @NotNull Map<Key, Object> changeContext) { if (!canCreate(project, change)) return null; return new ChangeDiffRequestProducer(project, change, changeContext); } public static boolean canCreate(@Nullable Project project, @NotNull Change change) { for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) { if (provider.canCreate(project, change)) return true; } for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) { if (provider.canCreate(project, change)) return true; } ContentRevision bRev = change.getBeforeRevision(); ContentRevision aRev = change.getAfterRevision(); if (bRev == null && aRev == null) return false; if (bRev != null && bRev.getFile().isDirectory()) return false; if (aRev != null && aRev.getFile().isDirectory()) return false; return true; } @NotNull @Override public DiffRequest process(@NotNull UserDataHolder context, @NotNull ProgressIndicator indicator) throws DiffRequestProducerException, ProcessCanceledException { try { return loadCurrentContents(context, indicator); } catch (ProcessCanceledException | DiffRequestProducerException e) { throw e; } catch (Exception e) { LOG.warn(e); throw new DiffRequestProducerException(e.getMessage()); } } @NotNull protected DiffRequest loadCurrentContents(@NotNull UserDataHolder context, @NotNull ProgressIndicator indicator) throws DiffRequestProducerException { DiffRequestProducerException wrapperException = null; DiffRequestProducerException requestException = null; DiffViewerWrapper wrapper = null; try { for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) { if (provider.canCreate(myProject, myChange)) { wrapper = provider.process(this, context, indicator); break; } } } catch (DiffRequestProducerException e) { wrapperException = e; } DiffRequest request = null; try { for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) { if (provider.canCreate(myProject, myChange)) { request = provider.process(this, context, indicator); break; } } if (request == null) request = createRequest(myProject, myChange, context, indicator); } catch (DiffRequestProducerException e) { requestException = e; } if (requestException != null && wrapperException != null) { String message = requestException.getMessage() + "\n\n" + wrapperException.getMessage(); throw new DiffRequestProducerException(message); } if (requestException != null) { request = new ErrorDiffRequest(getRequestTitle(myChange), requestException); LOG.info("Request: " + requestException.getMessage()); } if (wrapperException != null) { LOG.info("Wrapper: " + wrapperException.getMessage()); } request.putUserData(CHANGE_KEY, myChange); request.putUserData(DiffViewerWrapper.KEY, wrapper); for (Map.Entry<Key, Object> entry : myChangeContext.entrySet()) { request.putUserData(entry.getKey(), entry.getValue()); } DiffUtil.putDataKey(request, VcsDataKeys.CURRENT_CHANGE, myChange); return request; } @NotNull private DiffRequest createRequest(@Nullable Project project, @NotNull Change change, @NotNull UserDataHolder context, @NotNull ProgressIndicator indicator) throws DiffRequestProducerException { if (ChangesUtil.isTextConflictingChange(change)) { // three side diff // FIXME: This part is ugly as a VCS merge subsystem itself. FilePath path = ChangesUtil.getFilePath(change); VirtualFile file = path.getVirtualFile(); if (file == null) { file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.getPath()); } if (file == null) throw new DiffRequestProducerException("Can't show merge conflict - file not found"); if (project == null) { throw new DiffRequestProducerException("Can't show merge conflict - project is unknown"); } final AbstractVcs vcs = ChangesUtil.getVcsForChange(change, project); if (vcs == null || vcs.getMergeProvider() == null) { throw new DiffRequestProducerException("Can't show merge conflict - operation nos supported"); } try { // FIXME: loadRevisions() can call runProcessWithProgressSynchronously() inside final Ref<Throwable> exceptionRef = new Ref<>(); final Ref<MergeData> mergeDataRef = new Ref<>(); final VirtualFile finalFile = file; ApplicationManager.getApplication().invokeAndWait(() -> { try { mergeDataRef.set(vcs.getMergeProvider().loadRevisions(finalFile)); } catch (VcsException e) { exceptionRef.set(e); } }); if (!exceptionRef.isNull()) { Throwable e = exceptionRef.get(); if (e instanceof VcsException) throw (VcsException)e; if (e instanceof Error) throw (Error)e; if (e instanceof RuntimeException) throw (RuntimeException)e; throw new RuntimeException(e); } MergeData mergeData = mergeDataRef.get(); ContentRevision bRev = change.getBeforeRevision(); ContentRevision aRev = change.getAfterRevision(); String beforeRevisionTitle = getRevisionTitle(bRev, "Your version"); String afterRevisionTitle = getRevisionTitle(aRev, "Server version"); String title = DiffRequestFactory.getInstance().getTitle(file); List<String> titles = ContainerUtil.list(beforeRevisionTitle, "Base Version", afterRevisionTitle); DiffContentFactory contentFactory = DiffContentFactory.getInstance(); List<DiffContent> contents = ContainerUtil.list( contentFactory.createFromBytes(project, mergeData.CURRENT, file), contentFactory.createFromBytes(project, mergeData.ORIGINAL, file), contentFactory.createFromBytes(project, mergeData.LAST, file) ); SimpleDiffRequest request = new SimpleDiffRequest(title, contents, titles); MergeUtil.putRevisionInfos(request, mergeData); return request; } catch (VcsException | IOException e) { LOG.info(e); throw new DiffRequestProducerException(e); } } else { ContentRevision bRev = change.getBeforeRevision(); ContentRevision aRev = change.getAfterRevision(); if (bRev == null && aRev == null) { LOG.warn("Both revision contents are empty"); throw new DiffRequestProducerException("Bad revisions contents"); } if (bRev != null) checkContentRevision(project, bRev, context, indicator); if (aRev != null) checkContentRevision(project, aRev, context, indicator); String title = getRequestTitle(change); indicator.setIndeterminate(true); DiffContent content1 = createContent(project, bRev, context, indicator); DiffContent content2 = createContent(project, aRev, context, indicator); final String userLeftRevisionTitle = (String)myChangeContext.get(DiffUserDataKeysEx.VCS_DIFF_LEFT_CONTENT_TITLE); String beforeRevisionTitle = userLeftRevisionTitle != null ? userLeftRevisionTitle : getRevisionTitle(bRev, "Base version"); final String userRightRevisionTitle = (String)myChangeContext.get(DiffUserDataKeysEx.VCS_DIFF_RIGHT_CONTENT_TITLE); String afterRevisionTitle = userRightRevisionTitle != null ? userRightRevisionTitle : getRevisionTitle(aRev, "Your version"); SimpleDiffRequest request = new SimpleDiffRequest(title, content1, content2, beforeRevisionTitle, afterRevisionTitle); boolean bRevCurrent = bRev instanceof CurrentContentRevision; boolean aRevCurrent = aRev instanceof CurrentContentRevision; if (bRevCurrent && !aRevCurrent) request.putUserData(DiffUserDataKeys.MASTER_SIDE, Side.LEFT); if (!bRevCurrent && aRevCurrent) request.putUserData(DiffUserDataKeys.MASTER_SIDE, Side.RIGHT); return request; } } @NotNull public static String getRequestTitle(@NotNull Change change) { ContentRevision bRev = change.getBeforeRevision(); ContentRevision aRev = change.getAfterRevision(); FilePath bPath = bRev != null ? bRev.getFile() : null; FilePath aPath = aRev != null ? aRev.getFile() : null; return DiffRequestFactoryImpl.getTitle(bPath, aPath, " -> "); } @NotNull public static String getRevisionTitle(@Nullable ContentRevision revision, @NotNull String defaultValue) { if (revision == null) return defaultValue; String title = revision.getRevisionNumber().asString(); if (title == null || title.isEmpty()) return defaultValue; return title; } @NotNull public static DiffContent createContent(@Nullable Project project, @Nullable ContentRevision revision, @NotNull UserDataHolder context, @NotNull ProgressIndicator indicator) throws DiffRequestProducerException { try { indicator.checkCanceled(); if (revision == null) return DiffContentFactory.getInstance().createEmpty(); FilePath filePath = revision.getFile(); DiffContentFactoryEx contentFactory = DiffContentFactoryEx.getInstanceEx(); if (revision instanceof CurrentContentRevision) { VirtualFile vFile = ((CurrentContentRevision)revision).getVirtualFile(); if (vFile == null) throw new DiffRequestProducerException("Can't get current revision content"); return contentFactory.create(project, vFile); } if (revision instanceof ByteBackedContentRevision) { byte[] revisionContent = ((ByteBackedContentRevision)revision).getContentAsBytes(); if (revisionContent == null) throw new DiffRequestProducerException("Can't get revision content"); return contentFactory.createFromBytes(project, revisionContent, filePath); } else { String revisionContent = revision.getContent(); if (revisionContent == null) throw new DiffRequestProducerException("Can't get revision content"); return contentFactory.create(project, revisionContent, filePath); } } catch (IOException | VcsException e) { LOG.info(e); throw new DiffRequestProducerException(e); } } public static void checkContentRevision(@Nullable Project project, @NotNull ContentRevision rev, @NotNull UserDataHolder context, @NotNull ProgressIndicator indicator) throws DiffRequestProducerException { if (rev.getFile().isDirectory()) { throw new DiffRequestProducerException("Can't show diff for directory"); } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ChangeDiffRequestProducer that = (ChangeDiffRequestProducer)o; return myChange.equals(that.myChange); } @Override public int hashCode() { return myChange.hashCode(); } }