[patch]: do best attempt to apply patch

* store failed patches;
* if one filePatch has FAILED status it means we need to continue but store this info;
* process added/deleted files and mark them as failed if preCheck failed;
* refactor path verifier;
* notify about failed status with detailed dialog info and rollback button (not implemented yet);
* add labels to easily distinguish 'apply patch action' in local history dialog;
This commit is contained in:
Nadya Zabrodina
2016-01-25 17:05:15 +03:00
parent 740f69c2e2
commit 7109775e39
3 changed files with 215 additions and 73 deletions
@@ -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<BinaryType extends FilePatch> {
private final CommitContext myCommitContext;
private final Consumer<Collection<FilePath>> myToTargetListsMover;
@NotNull private final List<FilePatch> myRemainingPatches;
@NotNull private final List<FilePatch> myFailedPatches;
private final PathsVerifier<BinaryType> myVerifier;
private boolean mySystemOperation;
@@ -84,6 +84,7 @@ public class PatchApplier<BinaryType extends FilePatch> {
myLeftConflictPanelTitle = leftConflictPanelTitle;
myRightConflictPanelTitle = rightConflictPanelTitle;
myRemainingPatches = new ArrayList<FilePatch>();
myFailedPatches = new ArrayList<FilePatch>();
myVerifier = new PathsVerifier<BinaryType>(myProject, myBaseDirectory, myPatches, new PathsVerifier.BaseMapper() {
@Override
@Nullable
@@ -132,6 +133,11 @@ public class PatchApplier<BinaryType extends FilePatch> {
return myPatches;
}
@NotNull
private Collection<FilePatch> getFailedPatches() {
return myFailedPatches;
}
@NotNull
public List<BinaryType> getBinaryPatches() {
return ContainerUtil.mapNotNull(myVerifier.getBinaryPatches(),
@@ -165,17 +171,17 @@ public class PatchApplier<BinaryType extends FilePatch> {
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.<PatchApplier>singletonList(PatchApplier.this), beforeLabel);
}
if(myShowNotification || !ApplyPatchStatus.SUCCESS.equals(myStatus)) {
showApplyStatus(myProject, myStatus);
}
@@ -185,19 +191,16 @@ public class PatchApplier<BinaryType extends FilePatch> {
@CalledInAwt
@NotNull
private ApplyPatchStatus getApplyPatchStatus(@NotNull final TriggerAdditionOrDeletion trigger) {
final Ref<ApplyPatchStatus> refStatus = Ref.create(ApplyPatchStatus.FAILURE);
final Ref<ApplyPatchStatus> 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<BinaryType extends FilePatch> {
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<ApplyPatchStatus> refStatus = new Ref<ApplyPatchStatus>(null);
final Ref<ApplyPatchStatus> refStatus = new Ref<ApplyPatchStatus>(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<BinaryType extends FilePatch> {
}, 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<BinaryType extends FilePatch> {
directlyAffected.addAll(trigger.getAffected());
final Consumer<Collection<FilePath>> 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<PatchApplier> group, @NotNull Label beforeLabel) {
Collection<FilePatch> allFailed = ContainerUtil.concat(group, new Function<PatchApplier, Collection<? extends FilePatch>>() {
@Override
public Collection<FilePatch> fun(PatchApplier applier) {
return applier.getFailedPatches();
}
});
UndoApplyPatchDialog.rollbackApplyPatch(project, ContainerUtil.map(allFailed, new Function<FilePatch, FilePath>() {
@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<FilePatch> failedPreCheck = myVerifier.nonWriteActionPreCheck();
myFailedPatches.addAll(failedPreCheck);
myPatches.removeAll(failedPreCheck);
final List<FilePatch> 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<Pair<VirtualFile, ApplyTextFilePatch>> 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>() {
Boolean isSuccess = application.runWriteAction(new Computable<Boolean>() {
@Override
public Boolean compute() {
return myVerifier.execute();
final List<FilePatch> filePatches = myVerifier.execute();
myFailedPatches.addAll(filePatches);
myPatches.removeAll(filePatches);
return myFailedPatches.isEmpty();
}
});
return isSuccess ? ApplyPatchStatus.SUCCESS : ApplyPatchStatus.FAILURE;
}
private static void markInternalOperation(List<Pair<VirtualFile, ApplyTextFilePatch>> textPatches, boolean set) {
@@ -404,20 +432,21 @@ public class PatchApplier<BinaryType extends FilePatch> {
}
@Nullable
private ApplyPatchStatus actualApply(final PathsVerifier<BinaryType> verifier, final CommitContext commitContext) {
final List<Pair<VirtualFile, ApplyTextFilePatch>> textPatches = verifier.getTextPatches();
private ApplyPatchStatus actualApply(final List<Pair<VirtualFile, ApplyTextFilePatch>> textPatches,
final List<Pair<VirtualFile, ApplyFilePatchBase<BinaryType>>> 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<Pair<VirtualFile, ApplyFilePatchBase<BinaryType>>> binaryPatches = verifier.getBinaryPatches();
status = applyList(binaryPatches, context, status, commitContext);
}
else {
ApplyPatchStatus patchStatus = myCustomForBinaries.apply(binaryPatches);
final List<FilePatch> appliedPatches = myCustomForBinaries.getAppliedPatches();
moveForCustomBinaries(binaryPatches, appliedPatches);
@@ -428,7 +457,7 @@ public class PatchApplier<BinaryType extends FilePatch> {
}
catch (IOException e) {
showError(myProject, e.getMessage(), true);
return ApplyPatchStatus.FAILURE;
return ApplyPatchStatus.ABORT;
}
return status;
}
@@ -452,7 +481,10 @@ public class PatchApplier<BinaryType extends FilePatch> {
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<BinaryType extends FilePatch> {
return ReadonlyStatusHandler.getInstance(myProject).ensureFilesWritable(fileArray);
}
private boolean fileTypesAreOk(final List<Pair<VirtualFile, ApplyTextFilePatch>> textPatches) {
for (Pair<VirtualFile, ApplyTextFilePatch> 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()) {
@@ -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<BinaryType extends FilePatch> {
}
@CalledInAwt
public boolean nonWriteActionPreCheck() {
public List<FilePatch> nonWriteActionPreCheck() {
List<FilePatch> 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<FilePatch> skipped = myDelayedPrecheckContext.doDelayed();
mySkipped.addAll(skipped);
myPatches.removeAll(skipped);
return true;
myPatches.removeAll(failedToApply);
return failedToApply;
}
public List<FilePatch> getSkipped() {
return mySkipped;
}
public boolean execute() {
public List<FilePatch> execute() {
List<FilePatch> failedPatches = ContainerUtil.newArrayList();
try {
final List<CheckPath> checkers = new ArrayList<CheckPath>(myPatches.size());
for (FilePatch patch : myPatches) {
@@ -149,17 +158,17 @@ public class PathsVerifier<BinaryType extends FilePatch> {
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<BinaryType extends FilePatch> {
return myDeletedPaths;
}
@NotNull
public Collection<FilePatch> filterBadFileTypePatches() {
List<Pair<VirtualFile, ApplyTextFilePatch>> failedTextPatches =
ContainerUtil.findAll(myTextPatches, new Condition<Pair<VirtualFile, ApplyTextFilePatch>>() {
@Override
public boolean value(Pair<VirtualFile, ApplyTextFilePatch> textPatch) {
final VirtualFile file = textPatch.getFirst();
if (file.isDirectory()) return false;
return !isFileTypeOk(file);
}
});
myTextPatches.removeAll(failedTextPatches);
return ContainerUtil.map(failedTextPatches, new Function<Pair<VirtualFile, ApplyTextFilePatch>, FilePatch>() {
@Override
public FilePatch fun(Pair<VirtualFile, ApplyTextFilePatch> 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<BinaryType extends FilePatch> {
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<BinaryType extends FilePatch> {
}
private void revert(final String errorMessage) {
//todo show notification balloon
PatchApplier.showError(myProject, errorMessage, true);
// move back
@@ -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<FilePath> myFailedFilePaths;
private final Label myBeforeLabel;
private final Project myProject;
private UndoApplyPatchDialog(@NotNull Project project, @NotNull List<FilePath> 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<FilePath> filePaths,
@NotNull Label historyLabel) {
new UndoApplyPatchDialog(project, filePaths, historyLabel).show();
}
}