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)
This commit is contained in:
irengrig
2012-10-26 15:09:15 +04:00
parent ee060f5c6c
commit 127ca7ff7a
3 changed files with 310 additions and 2 deletions
@@ -109,6 +109,25 @@ public class GenericPatchApplier {
}
}
public int weightContextMatch(final int maxWalk, final int maxPartsToCheck) {
final List<SplitHunk> hunks = new ArrayList<SplitHunk>(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()) {
@@ -218,7 +218,7 @@ public class ApplyPatchDifferentiatedDialog extends DialogWrapper {
}
private void init(List<TextFilePatch> patches, final LocalChangeList localChangeList) {
final List<FilePatchInProgress> matchedPathes = new AutoMatchIterator(myProject).execute(patches);
final List<FilePatchInProgress> 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<FilePatchInProgress> matchedPathes = patchReader == null ? Collections.<FilePatchInProgress>emptyList() :
new AutoMatchIterator(myProject).execute(patchReader.getPatches());
new MatchPatchPaths(myProject).execute(patchReader.getPatches());
SwingUtilities.invokeLater(new Runnable() {
public void run() {
@@ -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<FilePatchInProgress> execute(final List<TextFilePatch> list) {
final PatchBaseDirectoryDetector directoryDetector = PatchBaseDirectoryDetector.getInstance(myProject);
final Application application = ApplicationManager.getApplication();
final List<PatchAndVariants> candidates = new ArrayList<PatchAndVariants>(list.size());
final List<TextFilePatch> newOrWithoutMatches = new ArrayList<TextFilePatch>();
findCandidates(list, directoryDetector, application, candidates, newOrWithoutMatches);
final MultiMap<VirtualFile, FilePatchInProgress> result = new MultiMap<VirtualFile, FilePatchInProgress>();
// 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<FilePatchInProgress>(result.values());
}
private void workWithNotExisting(PatchBaseDirectoryDetector directoryDetector,
List<TextFilePatch> newOrWithoutMatches,
MultiMap<VirtualFile, FilePatchInProgress> result) {
for (TextFilePatch patch : newOrWithoutMatches) {
final String[] strings = patch.getAfterName().replace('\\', '/').split("/");
Pair<VirtualFile, Integer> best = null;
for (int i = strings.length - 2; i >= 0; -- i) {
final String name = strings[i];
final Collection<VirtualFile> files = directoryDetector.findFiles(name);
if (! files.isEmpty()) {
// check all candidates
for (VirtualFile file : files) {
Pair<VirtualFile, Integer> 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<PatchAndVariants> candidates, MultiMap<VirtualFile, FilePatchInProgress> result) {
for (Iterator<PatchAndVariants> 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<PatchAndVariants> candidates, MultiMap<VirtualFile, FilePatchInProgress> result) {
for (Iterator<PatchAndVariants> 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<FilePatchInProgress> exact = new ArrayList<FilePatchInProgress>(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<TextFilePatch> list,
final PatchBaseDirectoryDetector directoryDetector,
Application application,
List<PatchAndVariants> candidates, List<TextFilePatch> newOrWithoutMatches) {
for (final TextFilePatch patch : list) {
final String fileName = patch.getBeforeFileName();
if (patch.isNewFile() || (patch.getBeforeName() == null)) {
newOrWithoutMatches.add(patch);
continue;
}
final Collection<VirtualFile> files = application.runReadAction(new Computable<Collection<VirtualFile>>() {
public Collection<VirtualFile> 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<FilePatchInProgress> variants = ObjectsConvertor.convert(files, new Convertor<VirtualFile, FilePatchInProgress>() {
@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<VirtualFile, FilePatchInProgress> result,
final List<FilePatchInProgress> variants,
FilePatchInProgress patchInProgress) {
patchInProgress.setAutoBases(ObjectsConvertor.convert(variants, new Convertor<FilePatchInProgress, VirtualFile>() {
@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<FilePatchInProgress> myVariants;
private PatchAndVariants(TextFilePatch patch, List<FilePatchInProgress> variants) {
myPatch = patch;
myVariants = variants;
}
public TextFilePatch getPatch() {
return myPatch;
}
public List<FilePatchInProgress> getVariants() {
return myVariants;
}
}
private Pair<VirtualFile, Integer> 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<VirtualFile, Integer> compareNamesImpl(String[] parts, VirtualFile parent, int idx) {
VirtualFile previous = parent;
while ((parent != null) && (idx >= 0)) {
if (! parent.getName().equals(parts[idx])) {
return new Pair<VirtualFile, Integer>(parent, idx + 1);
}
previous = parent;
parent = parent.getParent();
-- idx;
}
return new Pair<VirtualFile, Integer>(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<VirtualFile, Integer> 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;
}
}