diff --git a/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/PatchApplier.java b/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/PatchApplier.java index 94e3814f62e0..c0c4d73f9672 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/PatchApplier.java +++ b/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/PatchApplier.java @@ -15,6 +15,8 @@ */ package com.intellij.openapi.diff.impl.patch.formove; +import com.intellij.history.Label; +import com.intellij.history.LocalHistory; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; @@ -23,9 +25,6 @@ import com.intellij.openapi.diff.impl.patch.ApplyPatchStatus; import com.intellij.openapi.diff.impl.patch.FilePatch; import com.intellij.openapi.diff.impl.patch.apply.ApplyFilePatchBase; import com.intellij.openapi.diff.impl.patch.apply.ApplyTextFilePatch; -import com.intellij.openapi.fileTypes.FileType; -import com.intellij.openapi.fileTypes.FileTypes; -import com.intellij.openapi.fileTypes.ex.FileTypeChooser; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.Messages; @@ -63,6 +62,7 @@ public class PatchApplier { private final CommitContext myCommitContext; private final Consumer> myToTargetListsMover; @NotNull private final List myRemainingPatches; + @NotNull private final List myFailedPatches; private final PathsVerifier myVerifier; private boolean mySystemOperation; @@ -84,6 +84,7 @@ public class PatchApplier { myLeftConflictPanelTitle = leftConflictPanelTitle; myRightConflictPanelTitle = rightConflictPanelTitle; myRemainingPatches = new ArrayList(); + myFailedPatches = new ArrayList(); myVerifier = new PathsVerifier(myProject, myBaseDirectory, myPatches, new PathsVerifier.BaseMapper() { @Override @Nullable @@ -132,6 +133,11 @@ public class PatchApplier { return myPatches; } + @NotNull + private Collection getFailedPatches() { + return myFailedPatches; + } + @NotNull public List getBinaryPatches() { return ContainerUtil.mapNotNull(myVerifier.getBinaryPatches(), @@ -165,17 +171,17 @@ public class PatchApplier { myRemainingPatches.addAll(myPatches); final ApplyPatchStatus patchStatus = nonWriteActionPreCheck(); - if (ApplyPatchStatus.FAILURE.equals(patchStatus)) { - myStatus = patchStatus; - return; - } - + final Label beforeLabel = LocalHistory.getInstance().putSystemLabel(myProject, "Before patch"); final TriggerAdditionOrDeletion trigger = new TriggerAdditionOrDeletion(myProject); final ApplyPatchStatus applyStatus = getApplyPatchStatus(trigger); myStatus = ApplyPatchStatus.SUCCESS.equals(patchStatus) ? applyStatus : ApplyPatchStatus.and(patchStatus, applyStatus); // listeners finished, all 'legal' file additions/deletions with VCS are done trigger.processIt(); + LocalHistory.getInstance().putSystemLabel(myProject, "After patch"); // insert a label to be visible in local history dialog + if (myStatus == ApplyPatchStatus.FAILURE || myStatus == ApplyPatchStatus.ABORT) { + suggestRollback(myProject, Collections.singletonList(PatchApplier.this), beforeLabel); + } if(myShowNotification || !ApplyPatchStatus.SUCCESS.equals(myStatus)) { showApplyStatus(myProject, myStatus); } @@ -185,19 +191,16 @@ public class PatchApplier { @CalledInAwt @NotNull private ApplyPatchStatus getApplyPatchStatus(@NotNull final TriggerAdditionOrDeletion trigger) { - final Ref refStatus = Ref.create(ApplyPatchStatus.FAILURE); + final Ref refStatus = Ref.create(ApplyPatchStatus.SUCCESS); try { setConfirmationToDefault(); CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { @Override public void run() { - if (!createFiles()) { - refStatus.set(ApplyPatchStatus.FAILURE); - return; - } + refStatus.set(createFiles()); addSkippedItems(trigger); trigger.prepare(); - refStatus.set(executeWritable()); + refStatus.set(ApplyPatchStatus.and(refStatus.get(), executeWritable())); } }, VcsBundle.message("patch.apply.command"), null); } @@ -253,19 +256,16 @@ public class PatchApplier { ApplyPatchStatus result = ApplyPatchStatus.SUCCESS; for (PatchApplier patchApplier : group) { result = ApplyPatchStatus.and(result, patchApplier.nonWriteActionPreCheck()); - if (ApplyPatchStatus.FAILURE.equals(result)) return result; } + final Label beforeLabel = LocalHistory.getInstance().putSystemLabel(project, "Before patch"); final TriggerAdditionOrDeletion trigger = new TriggerAdditionOrDeletion(project); - final Ref refStatus = new Ref(null); + final Ref refStatus = new Ref(result); try { CommandProcessor.getInstance().executeCommand(project, new Runnable() { @Override public void run() { for (PatchApplier applier : group) { - if (! applier.createFiles()) { - refStatus.set(ApplyPatchStatus.FAILURE); - return; - } + refStatus.set(ApplyPatchStatus.and(refStatus.get(), applier.createFiles())); applier.addSkippedItems(trigger); } trigger.prepare(); @@ -277,6 +277,7 @@ public class PatchApplier { }, VcsBundle.message("patch.apply.command"), null); } finally { VcsFileListenerContextHelper.getInstance(project).clearContext(); + LocalHistory.getInstance().putSystemLabel(project, "After patch"); } result = refStatus.get(); result = result == null ? ApplyPatchStatus.FAILURE : result; @@ -291,54 +292,81 @@ public class PatchApplier { directlyAffected.addAll(trigger.getAffected()); final Consumer> mover = localChangeList == null ? null : createMover(project, localChangeList); refreshPassedFilesAndMoveToChangelist(project, directlyAffected, indirectlyAffected, mover); + if (result == ApplyPatchStatus.FAILURE || result == ApplyPatchStatus.ABORT) { + suggestRollback(project, group, beforeLabel); + } showApplyStatus(project, result); return result; } + private static void suggestRollback(@NotNull Project project, @NotNull Collection group, @NotNull Label beforeLabel) { + Collection allFailed = ContainerUtil.concat(group, new Function>() { + @Override + public Collection fun(PatchApplier applier) { + return applier.getFailedPatches(); + } + }); + UndoApplyPatchDialog.rollbackApplyPatch(project, ContainerUtil.map(allFailed, new Function() { + @Override + public FilePath fun(FilePatch filePatch) { + String path = + filePatch.getAfterName() == null + ? filePatch.getBeforeName() + : filePatch.getAfterName(); + return VcsUtil.getFilePath(path); + } + }), beforeLabel); + } + protected void addSkippedItems(final TriggerAdditionOrDeletion trigger) { trigger.addExisting(myVerifier.getToBeAdded()); trigger.addDeleted(myVerifier.getToBeDeleted()); } public ApplyPatchStatus nonWriteActionPreCheck() { - final boolean value = myVerifier.nonWriteActionPreCheck(); - if (! value) return ApplyPatchStatus.FAILURE; - + final List failedPreCheck = myVerifier.nonWriteActionPreCheck(); + myFailedPatches.addAll(failedPreCheck); + myPatches.removeAll(failedPreCheck); final List skipped = myVerifier.getSkipped(); final boolean applyAll = skipped.isEmpty(); myPatches.removeAll(skipped); - return applyAll ? ApplyPatchStatus.SUCCESS : ((skipped.size() == myPatches.size()) ? ApplyPatchStatus.ALREADY_APPLIED : ApplyPatchStatus.PARTIAL) ; + if (!failedPreCheck.isEmpty()) return ApplyPatchStatus.FAILURE; + return applyAll + ? ApplyPatchStatus.SUCCESS + : ((skipped.size() == myPatches.size()) ? ApplyPatchStatus.ALREADY_APPLIED : ApplyPatchStatus.PARTIAL); } protected ApplyPatchStatus executeWritable() { final ReadonlyStatusHandler.OperationStatus readOnlyFilesStatus = getReadOnlyFilesStatus(myVerifier.getWritableFiles()); if (readOnlyFilesStatus.hasReadonlyFiles()) { showError(myProject, readOnlyFilesStatus.getReadonlyFilesMessage(), true); - return ApplyPatchStatus.FAILURE; + return ApplyPatchStatus.ABORT; } - + myFailedPatches.addAll(myVerifier.filterBadFileTypePatches()); + ApplyPatchStatus result = myFailedPatches.isEmpty() ? ApplyPatchStatus.SUCCESS : ApplyPatchStatus.FAILURE; final List> textPatches = myVerifier.getTextPatches(); - if (!fileTypesAreOk(textPatches)) { - return ApplyPatchStatus.FAILURE; - } - try { markInternalOperation(textPatches, true); - return actualApply(myVerifier, myCommitContext); + return ApplyPatchStatus.and(result, actualApply(textPatches, myVerifier.getBinaryPatches(), myCommitContext)); } finally { markInternalOperation(textPatches, false); } } - private boolean createFiles() { + @NotNull + private ApplyPatchStatus createFiles() { final Application application = ApplicationManager.getApplication(); - return application.runWriteAction(new Computable() { + Boolean isSuccess = application.runWriteAction(new Computable() { @Override public Boolean compute() { - return myVerifier.execute(); + final List filePatches = myVerifier.execute(); + myFailedPatches.addAll(filePatches); + myPatches.removeAll(filePatches); + return myFailedPatches.isEmpty(); } }); + return isSuccess ? ApplyPatchStatus.SUCCESS : ApplyPatchStatus.FAILURE; } private static void markInternalOperation(List> textPatches, boolean set) { @@ -404,20 +432,21 @@ public class PatchApplier { } @Nullable - private ApplyPatchStatus actualApply(final PathsVerifier verifier, final CommitContext commitContext) { - final List> textPatches = verifier.getTextPatches(); + private ApplyPatchStatus actualApply(final List> textPatches, + final List>> binaryPatches, + final CommitContext commitContext) { final ApplyPatchContext context = new ApplyPatchContext(myBaseDirectory, 0, true, true); - ApplyPatchStatus status = null; + ApplyPatchStatus status; try { - status = applyList(textPatches, context, status, commitContext); + status = applyList(textPatches, context, null, commitContext); if (status == ApplyPatchStatus.ABORT) return status; if (myCustomForBinaries == null) { - status = applyList(verifier.getBinaryPatches(), context, status, commitContext); - } else { - final List>> binaryPatches = verifier.getBinaryPatches(); + status = applyList(binaryPatches, context, status, commitContext); + } + else { ApplyPatchStatus patchStatus = myCustomForBinaries.apply(binaryPatches); final List appliedPatches = myCustomForBinaries.getAppliedPatches(); moveForCustomBinaries(binaryPatches, appliedPatches); @@ -428,7 +457,7 @@ public class PatchApplier { } catch (IOException e) { showError(myProject, e.getMessage(), true); - return ApplyPatchStatus.FAILURE; + return ApplyPatchStatus.ABORT; } return status; } @@ -452,7 +481,10 @@ public class PatchApplier { if (patchStatus == ApplyPatchStatus.ABORT) return patchStatus; status = ApplyPatchStatus.and(status, patchStatus); - if (patchStatus == ApplyPatchStatus.FAILURE) return status; + if (patchStatus == ApplyPatchStatus.FAILURE) { + myFailedPatches.add(patch.getSecond().getPatch()); + continue; + } if (patchStatus != ApplyPatchStatus.SKIP) { myVerifier.doMoveIfNeeded(patch.getFirst()); myRemainingPatches.remove(patch.getSecond().getPatch()); @@ -483,27 +515,6 @@ public class PatchApplier { return ReadonlyStatusHandler.getInstance(myProject).ensureFilesWritable(fileArray); } - private boolean fileTypesAreOk(final List> textPatches) { - for (Pair textPatch : textPatches) { - final VirtualFile file = textPatch.getFirst(); - if (! file.isDirectory()) { - FileType fileType = file.getFileType(); - if (fileType == FileTypes.UNKNOWN) { - fileType = FileTypeChooser.associateFileType(file.getName()); - if (fileType == null) { - showError(myProject, "Cannot apply patch. File " + file.getPresentableName() + " type not defined.", true); - return false; - } - } - if (fileType.isBinary()) { - showError(myProject, "Cannot apply patch because it contains binary file " + file.getPresentableName(), true); - return false; - } - } - } - return true; - } - public static void showError(final Project project, final String message, final boolean error) { final Application application = ApplicationManager.getApplication(); if (application.isUnitTestMode()) { diff --git a/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/PathsVerifier.java b/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/PathsVerifier.java index 9954cdd344a3..7a1e242354b8 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/PathsVerifier.java +++ b/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/PathsVerifier.java @@ -22,8 +22,12 @@ import com.intellij.openapi.diff.impl.patch.TextFilePatch; import com.intellij.openapi.diff.impl.patch.apply.ApplyFilePatchBase; import com.intellij.openapi.diff.impl.patch.apply.ApplyFilePatchFactory; import com.intellij.openapi.diff.impl.patch.apply.ApplyTextFilePatch; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.FileTypes; +import com.intellij.openapi.fileTypes.ex.FileTypeChooser; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.ThrowableComputable; import com.intellij.openapi.util.io.FileUtil; @@ -34,6 +38,8 @@ import com.intellij.openapi.vcs.changes.shelf.ShelvedBinaryFilePatch; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; import com.intellij.vcsUtil.VcsUtil; import org.jetbrains.annotations.CalledInAwt; import org.jetbrains.annotations.NotNull; @@ -122,26 +128,29 @@ public class PathsVerifier { } @CalledInAwt - public boolean nonWriteActionPreCheck() { + public List nonWriteActionPreCheck() { + List failedToApply = ContainerUtil.newArrayList(); myDelayedPrecheckContext = new DelayedPrecheckContext(myProject); for (FilePatch patch : myPatches) { final CheckPath checker = getChecker(patch); - if (! checker.canBeApplied(myDelayedPrecheckContext)) { + if (!checker.canBeApplied(myDelayedPrecheckContext)) { revert(checker.getErrorMessage()); - return false; + failedToApply.add(patch); } } final Collection skipped = myDelayedPrecheckContext.doDelayed(); mySkipped.addAll(skipped); myPatches.removeAll(skipped); - return true; + myPatches.removeAll(failedToApply); + return failedToApply; } public List getSkipped() { return mySkipped; } - public boolean execute() { + public List execute() { + List failedPatches = ContainerUtil.newArrayList(); try { final List checkers = new ArrayList(myPatches.size()); for (FilePatch patch : myPatches) { @@ -149,17 +158,17 @@ public class PathsVerifier { checkers.add(checker); } for (CheckPath checker : checkers) { - if (! checker.check()) { + if (!checker.check()) { + failedPatches.add(checker.getPatch()); revert(checker.getErrorMessage()); - return false; } } - return true; } catch (IOException e) { revert(e.getMessage()); - return false; } + myPatches.removeAll(failedPatches); + return failedPatches; } private CheckPath getChecker(final FilePatch patch) { @@ -187,6 +196,44 @@ public class PathsVerifier { return myDeletedPaths; } + @NotNull + public Collection filterBadFileTypePatches() { + List> failedTextPatches = + ContainerUtil.findAll(myTextPatches, new Condition>() { + @Override + public boolean value(Pair textPatch) { + final VirtualFile file = textPatch.getFirst(); + if (file.isDirectory()) return false; + return !isFileTypeOk(file); + } + }); + myTextPatches.removeAll(failedTextPatches); + return ContainerUtil.map(failedTextPatches, new Function, FilePatch>() { + @Override + public FilePatch fun(Pair patchInfo) { + return patchInfo.getSecond().getPatch(); + } + }); + } + + private boolean isFileTypeOk(@NotNull VirtualFile file) { + FileType fileType = file.getFileType(); + if (fileType == FileTypes.UNKNOWN) { + fileType = FileTypeChooser.associateFileType(file.getName()); + if (fileType == null) { + PatchApplier + .showError(myProject, + "Cannot apply file " + file.getPresentableName() + " from patch because its type not defined.", true); + return false; + } + } + if (fileType.isBinary()) { + PatchApplier.showError(myProject, "Cannot apply file " + file.getPresentableName() + " from patch because it is binary.", true); + return false; + } + return true; + } + private class CheckModified extends CheckDeleted { private CheckModified(final FilePatch path) { super(path); @@ -343,6 +390,10 @@ public class PathsVerifier { private boolean inContent(VirtualFile file) { return myVcsManager.isFileInContent(file); } + + public FilePatch getPatch() { + return myPatch; + } } private void addPatch(final FilePatch patch, final VirtualFile file) { @@ -369,6 +420,7 @@ public class PathsVerifier { } private void revert(final String errorMessage) { + //todo show notification balloon PatchApplier.showError(myProject, errorMessage, true); // move back diff --git a/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/UndoApplyPatchDialog.java b/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/UndoApplyPatchDialog.java new file mode 100644 index 000000000000..617767072842 --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/formove/UndoApplyPatchDialog.java @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2015 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.diff.impl.patch.formove; + +import com.intellij.history.Label; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.changes.ui.FilePathChangesTreeList; +import com.intellij.ui.components.JBLabel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +class UndoApplyPatchDialog extends DialogWrapper { + + + private final List myFailedFilePaths; + private final Label myBeforeLabel; + private final Project myProject; + + private UndoApplyPatchDialog(@NotNull Project project, @NotNull List filePaths, @NotNull Label beforeLabel) { + super(project, true); + myProject = project; + setTitle("Patch Applying Partly Failed"); + setOKButtonText("Rollback"); + myFailedFilePaths = filePaths; + myBeforeLabel = beforeLabel; + init(); + } + + @Nullable + @Override + protected JComponent createCenterPanel() { + final JPanel panel = new JPanel(new BorderLayout()); + int numFiles = myFailedFilePaths.size(); + String detailedText = numFiles == 0 ? "" : String.format("Failed to apply %s below. ", StringUtil.pluralize("file", numFiles)); + final JLabel infoLabel = new JBLabel(detailedText + "Would you like to rollback all applied?"); + panel.add(infoLabel, BorderLayout.NORTH); + if (numFiles > 0) { + FilePathChangesTreeList browser = new FilePathChangesTreeList(myProject, myFailedFilePaths, false, false, null, null) { + @Override + public Dimension getPreferredSize() { + return new Dimension(infoLabel.getPreferredSize().width, 50); + } + }; + browser.setChangesToDisplay(myFailedFilePaths); + panel.add(browser, BorderLayout.CENTER); + } + return panel; + } + + @Override + protected void doOKAction() { + super.doOKAction(); + } + + static void rollbackApplyPatch(@NotNull Project project, @NotNull List filePaths, + @NotNull Label historyLabel) { + new UndoApplyPatchDialog(project, filePaths, historyLabel).show(); + } +}