From 127ca7ff7aa585dabbbdb3487da5ce728001eef0 Mon Sep 17 00:00:00 2001 From: irengrig Date: Fri, 26 Oct 2012 15:09:15 +0400 Subject: [PATCH] Apply patch: new algorithm for candidate files search: 1) look for by file name if there are several files with that name, select between them: 2) select those with full path matches (a/b/c/d/file) 3) make a look inside a patch and check [partially] whether context could be found and weight matching if there're relative paths (it is possible when you create patch from IDEA and the root is outside the project directory), try to find by exact path starting from project directory. It will fix cases when we have versioned directories but not under project scope (can not use index to search there) will fix IDEA-56110 "Apply patch" applies changes to the wrong file. and IDEA-53865 "Failed to apply patch for file ...: couldn't find context" (wrong base directory guess case) --- .../impl/patch/apply/GenericPatchApplier.java | 19 ++ .../patch/ApplyPatchDifferentiatedDialog.java | 4 +- .../vcs/changes/patch/MatchPatchPaths.java | 289 ++++++++++++++++++ 3 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/MatchPatchPaths.java diff --git a/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/apply/GenericPatchApplier.java b/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/apply/GenericPatchApplier.java index 7084da5b6a7b..bba70e1f1ac8 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/apply/GenericPatchApplier.java +++ b/platform/vcs-impl/src/com/intellij/openapi/diff/impl/patch/apply/GenericPatchApplier.java @@ -109,6 +109,25 @@ public class GenericPatchApplier { } } + public int weightContextMatch(final int maxWalk, final int maxPartsToCheck) { + final List hunks = new ArrayList(myHunks.size()); + for (PatchHunk hunk : myHunks) { + hunks.addAll(SplitHunk.read(hunk)); + } + int cntPlus = 0; + int cnt = maxPartsToCheck; + for (SplitHunk hunk : hunks) { + final SplitHunk copy = createWithAllContextCopy(hunk); + if (copy.isInsertion()) continue; + if (testForPartialContextMatch(copy, new ExactMatchSolver(copy), maxWalk)) { + ++ cntPlus; + } + -- cnt; + if (cnt == 0) break; + } + return cntPlus; + } + public boolean execute() { debug("GenericPatchApplier execute started"); if (! myHunks.isEmpty()) { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/ApplyPatchDifferentiatedDialog.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/ApplyPatchDifferentiatedDialog.java index 5e7246955838..abc059d4c686 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/ApplyPatchDifferentiatedDialog.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/ApplyPatchDifferentiatedDialog.java @@ -218,7 +218,7 @@ public class ApplyPatchDifferentiatedDialog extends DialogWrapper { } private void init(List patches, final LocalChangeList localChangeList) { - final List matchedPathes = new AutoMatchIterator(myProject).execute(patches); + final List matchedPathes = new MatchPatchPaths(myProject).execute(patches); SwingUtilities.invokeLater(new Runnable() { public void run() { @@ -311,7 +311,7 @@ public class ApplyPatchDifferentiatedDialog extends DialogWrapper { if (patchReader == null) return; final List matchedPathes = patchReader == null ? Collections.emptyList() : - new AutoMatchIterator(myProject).execute(patchReader.getPatches()); + new MatchPatchPaths(myProject).execute(patchReader.getPatches()); SwingUtilities.invokeLater(new Runnable() { public void run() { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/MatchPatchPaths.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/MatchPatchPaths.java new file mode 100644 index 000000000000..7f8b2e821d7a --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/MatchPatchPaths.java @@ -0,0 +1,289 @@ +/* + * Copyright 2000-2012 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.patch; + +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diff.impl.patch.TextFilePatch; +import com.intellij.openapi.diff.impl.patch.apply.GenericPatchApplier; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vcs.ObjectsConvertor; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.Convertor; +import com.intellij.util.containers.MultiMap; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * Created with IntelliJ IDEA. + * User: Irina.Chernushina + * Date: 10/25/12 + * Time: 5:51 PM + */ +public class MatchPatchPaths { + private final int ourBigFileBound = 100000; + private final Project myProject; + private final VirtualFile myBaseDir; + + public MatchPatchPaths(Project project) { + myProject = project; + myBaseDir = myProject.getBaseDir(); + } + + public List execute(final List list) { + final PatchBaseDirectoryDetector directoryDetector = PatchBaseDirectoryDetector.getInstance(myProject); + final Application application = ApplicationManager.getApplication(); + + final List candidates = new ArrayList(list.size()); + final List newOrWithoutMatches = new ArrayList(); + findCandidates(list, directoryDetector, application, candidates, newOrWithoutMatches); + + final MultiMap result = new MultiMap(); + // process exact matches: if one, leave and extract. if several - leave only them + filterExactMatches(candidates, result); + + // partially check by context + selectByContext(candidates, result); + // created or no variants + workWithNotExisting(directoryDetector, newOrWithoutMatches, result); + return new ArrayList(result.values()); + } + + private void workWithNotExisting(PatchBaseDirectoryDetector directoryDetector, + List newOrWithoutMatches, + MultiMap result) { + for (TextFilePatch patch : newOrWithoutMatches) { + final String[] strings = patch.getAfterName().replace('\\', '/').split("/"); + Pair best = null; + for (int i = strings.length - 2; i >= 0; -- i) { + final String name = strings[i]; + final Collection files = directoryDetector.findFiles(name); + if (! files.isEmpty()) { + // check all candidates + for (VirtualFile file : files) { + Pair pair = compareNamesImpl(strings, file, i); + if (pair != null && pair.getSecond() < i) { + if (best == null || pair.getSecond() > best.getSecond()) { + best = pair; + } + } + } + // if we found some match for lower directory, lets stop with checking its parents.. + if (best != null) { + break; + } + } + } + if (best != null) { + final FilePatchInProgress patchInProgress = new FilePatchInProgress(patch, null, myBaseDir); + patchInProgress.setNewBase(best.getFirst()); + int numDown = best.getSecond(); + for (int i = 0; i < numDown; i++) { + patchInProgress.up(); + } + result.putValue(best.getFirst(), patchInProgress); + } else { + final FilePatchInProgress patchInProgress = new FilePatchInProgress(patch, null, myBaseDir); + result.putValue(myBaseDir, patchInProgress); + } + } + } + + private void selectByContext(List candidates, MultiMap result) { + for (Iterator iterator = candidates.iterator(); iterator.hasNext(); ) { + final PatchAndVariants candidate = iterator.next(); + int maxLines = -100; + FilePatchInProgress best = null; + for (FilePatchInProgress variant : candidate.getVariants()) { + final int lines = getMatchingLines(variant); + if (lines > maxLines) { + maxLines = lines; + best = variant; + } + } + putSelected(result, candidate.getVariants(), best); + } + } + + private void filterExactMatches(List candidates, MultiMap result) { + for (Iterator iterator = candidates.iterator(); iterator.hasNext(); ) { + final PatchAndVariants candidate = iterator.next(); + if (candidate.getVariants().size() == 1) { + final FilePatchInProgress oneCandidate = candidate.getVariants().get(0); + result.putValue(oneCandidate.getBase(), oneCandidate); + iterator.remove(); + } else { + final List exact = new ArrayList(candidate.getVariants().size()); + for (FilePatchInProgress patch : candidate.getVariants()) { + if (patch.getCurrentStrip() == 0) { + exact.add(patch); + } + } + if (exact.size() == 1) { + final FilePatchInProgress patchInProgress = exact.get(0); + putSelected(result, candidate.getVariants(), patchInProgress); + iterator.remove(); + } else if (! exact.isEmpty()) { + candidate.getVariants().retainAll(exact); + } + } + } + } + + private void findCandidates(List list, + final PatchBaseDirectoryDetector directoryDetector, + Application application, + List candidates, List newOrWithoutMatches) { + for (final TextFilePatch patch : list) { + final String fileName = patch.getBeforeFileName(); + if (patch.isNewFile() || (patch.getBeforeName() == null)) { + newOrWithoutMatches.add(patch); + continue; + } + final Collection files = application.runReadAction(new Computable>() { + public Collection compute() { + return directoryDetector.findFiles(fileName); + } + }); + // for directories outside the project scope but under version control + if (patch.getBeforeName() != null && patch.getBeforeName().startsWith("..")) { + final VirtualFile relativeFile = VfsUtil.findRelativeFile(myBaseDir, patch.getBeforeName().replace('\\', '/').split("/")); + if (relativeFile != null) { + files.add(relativeFile); + } + } + if (files.isEmpty()) { + newOrWithoutMatches.add(patch); + } else { + final List variants = ObjectsConvertor.convert(files, new Convertor() { + @Override + public FilePatchInProgress convert(VirtualFile o) { + return processMatch(patch, o); + } + }, ObjectsConvertor.NOT_NULL); + if (variants.isEmpty()) { + newOrWithoutMatches.add(patch); // just to be sure + } else { + candidates.add(new PatchAndVariants(patch, variants)); + } + } + } + } + + private void putSelected(MultiMap result, + final List variants, + FilePatchInProgress patchInProgress) { + patchInProgress.setAutoBases(ObjectsConvertor.convert(variants, new Convertor() { + @Override + public VirtualFile convert(FilePatchInProgress o) { + return o.getBase(); + } + }, ObjectsConvertor.NOT_NULL)); + result.putValue(patchInProgress.getBase(), patchInProgress); + } + + private int getMatchingLines(final FilePatchInProgress patch) { + final VirtualFile base = patch.getCurrentBase(); + if (base == null) return -1; + String text; + try { + if (base.getLength() > ourBigFileBound) { + // partially + text = VfsUtil.loadText(base, ourBigFileBound); + } else { + text = VfsUtil.loadText(base); + } + } + catch (IOException e) { + return 0; + } + return new GenericPatchApplier(text, patch.getPatch().getHunks()).weightContextMatch(100, 5); + } + + private static class PatchAndVariants { + private final TextFilePatch myPatch; + private final List myVariants; + + private PatchAndVariants(TextFilePatch patch, List variants) { + myPatch = patch; + myVariants = variants; + } + + public TextFilePatch getPatch() { + return myPatch; + } + + public List getVariants() { + return myVariants; + } + } + + private Pair compareNames(final String beforeName, final VirtualFile file) { + if (beforeName == null) return null; + final String[] parts = beforeName.replace('\\', '/').split("/"); + return compareNamesImpl(parts, file.getParent(), parts.length - 2); + } + + private Pair compareNamesImpl(String[] parts, VirtualFile parent, int idx) { + VirtualFile previous = parent; + while ((parent != null) && (idx >= 0)) { + if (! parent.getName().equals(parts[idx])) { + return new Pair(parent, idx + 1); + } + previous = parent; + parent = parent.getParent(); + -- idx; + } + return new Pair(parent, idx + 1); + } + + @Nullable + private FilePatchInProgress processMatch(final TextFilePatch patch, final VirtualFile file) { + final String beforeName = patch.getBeforeName(); + /*if (beforeName == null) return null; + final String[] parts = beforeName.replace('\\', '/').split("/"); + VirtualFile parent = file.getParent(); + int idx = parts.length - 2; + while ((parent != null) && (idx >= 0)) { + if (! parent.getName().equals(parts[idx])) { + break; + } + parent = parent.getParent(); + -- idx; + }*/ + final Pair pair = compareNames(beforeName, file); + if (pair == null) return null; + final VirtualFile parent = pair.getFirst(); + if (parent != null) { + final FilePatchInProgress result = new FilePatchInProgress(patch, null, myBaseDir); + result.setNewBase(parent); + int numDown = pair.getSecond(); + for (int i = 0; i < numDown; i++) { + result.up(); + } + return result; + } + return null; + } +}