blob: 0170fadb2d88acba73b657b3d34d07a165db9e98 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Martin Burger <m@rtin-burger.de> patch for #93810 and #93901
*******************************************************************************/
package org.eclipse.compare.internal.patch;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.compare.internal.core.Messages;
import org.eclipse.compare.internal.core.patch.DiffProject;
import org.eclipse.compare.internal.core.patch.FileDiffResult;
import org.eclipse.compare.internal.core.patch.FilePatch2;
import org.eclipse.compare.internal.core.patch.Hunk;
import org.eclipse.compare.internal.core.patch.PatchReader;
import org.eclipse.compare.patch.IHunk;
import org.eclipse.compare.patch.IHunkFilter;
import org.eclipse.compare.patch.PatchConfiguration;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.text.TextUtilities;
/**
* A Patcher
* - knows how to parse various patch file formats into some in-memory structure,
* - holds onto the parsed data and the options to use when applying the patches,
* - knows how to apply the patches to files and folders.
*/
public class Patcher implements IHunkFilter {
static protected final String REJECT_FILE_EXTENSION= ".rej"; //$NON-NLS-1$
static protected final String MARKER_TYPE= "org.eclipse.compare.rejectedPatchMarker"; //$NON-NLS-1$
/**
* Property used to associate a patcher with a {@link PatchConfiguration}
*/
public static final String PROP_PATCHER = "org.eclipse.compare.patcher"; //$NON-NLS-1$
public interface IFileValidator {
boolean validateResources(IFile[] array);
}
// diff formats
// private static final int CONTEXT= 0;
// private static final int ED= 1;
// private static final int NORMAL= 2;
// private static final int UNIFIED= 3;
private FilePatch2[] fDiffs;
private IResource fTarget;
// patch options
private Set<Object> disabledElements = new HashSet<>();
private Map<FilePatch2, FileDiffResult> diffResults = new HashMap<>();
private final Map<FilePatch2, byte[]> contentCache = new HashMap<>();
private Set<Hunk> mergedHunks = new HashSet<>();
private final PatchConfiguration configuration;
private boolean fGenerateRejectFile = false;
public Patcher() {
configuration = new PatchConfiguration();
configuration.setProperty(PROP_PATCHER, this);
configuration.addHunkFilter(this);
}
/*
* Returns an array of Diffs after a successful call to <code>parse</code>.
* If <code>parse</code> hasn't been called returns <code>null</code>.
*/
public FilePatch2[] getDiffs() {
if (fDiffs == null)
return new FilePatch2[0];
return fDiffs;
}
public IPath getPath(FilePatch2 diff) {
return diff.getStrippedPath(getStripPrefixSegments(), isReversed());
}
/*
* Returns <code>true</code> if new value differs from old.
*/
public boolean setStripPrefixSegments(int strip) {
if (strip != getConfiguration().getPrefixSegmentStripCount()) {
getConfiguration().setPrefixSegmentStripCount(strip);
return true;
}
return false;
}
int getStripPrefixSegments() {
return getConfiguration().getPrefixSegmentStripCount();
}
/*
* Returns <code>true</code> if new value differs from old.
*/
public boolean setFuzz(int fuzz) {
if (fuzz != getConfiguration().getFuzz()) {
getConfiguration().setFuzz(fuzz);
return true;
}
return false;
}
public int getFuzz(){
return getConfiguration().getFuzz();
}
/*
* Returns <code>true</code> if new value differs from old.
*/
public boolean setIgnoreWhitespace(boolean ignoreWhitespace) {
if (ignoreWhitespace != getConfiguration().isIgnoreWhitespace()) {
getConfiguration().setIgnoreWhitespace(ignoreWhitespace);
return true;
}
return false;
}
public boolean isIgnoreWhitespace() {
return getConfiguration().isIgnoreWhitespace();
}
public boolean isGenerateRejectFile() {
return fGenerateRejectFile;
}
public void setGenerateRejectFile(boolean generateRejectFile) {
fGenerateRejectFile = generateRejectFile;
}
//---- parsing patch files
public void parse(IStorage storage) throws IOException, CoreException {
BufferedReader reader = Utilities.createReader(storage);
try {
parse(reader);
} finally {
try {
reader.close();
} catch (IOException e) { //ignored
}
}
}
public void parse(BufferedReader reader) throws IOException {
PatchReader patchReader = new PatchReader() {
@Override
protected FilePatch2 createFileDiff(IPath oldPath, long oldDate,
IPath newPath, long newDate) {
return new FilePatch(oldPath, oldDate, newPath, newDate);
}
};
patchReader.parse(reader);
patchParsed(patchReader);
}
protected void patchParsed(PatchReader patchReader) {
fDiffs = patchReader.getDiffs();
}
public void countLines() {
FilePatch2[] fileDiffs = getDiffs();
for (FilePatch2 fileDiff : fileDiffs) {
int addedLines = 0;
int removedLines = 0;
for (int j = 0; j < fileDiff.getHunkCount(); j++) {
IHunk hunk = fileDiff.getHunks()[j];
String[] lines = ((Hunk) hunk).getLines();
for (String line : lines) {
char c = line.charAt(0);
switch (c) {
case '+':
addedLines++;
continue;
case '-':
removedLines++;
continue;
}
}
}
fileDiff.setAddedLines(addedLines);
fileDiff.setRemovedLines(removedLines);
}
}
//---- applying a patch file
public void applyAll(IProgressMonitor pm, IFileValidator validator) throws CoreException {
int i;
IFile singleFile= null; // file to be patched
IContainer container= null;
if (fTarget instanceof IContainer)
container= (IContainer) fTarget;
else if (fTarget instanceof IFile) {
singleFile= (IFile) fTarget;
container= singleFile.getParent();
} else {
Assert.isTrue(false);
}
// get all files to be modified in order to call validateEdit
List<IFile> list= new ArrayList<>();
if (singleFile != null)
list.add(singleFile);
else {
for (i= 0; i < fDiffs.length; i++) {
FilePatch2 diff= fDiffs[i];
if (isEnabled(diff)) {
switch (diff.getDiffType(isReversed())) {
case FilePatch2.CHANGE:
list.add(createPath(container, getPath(diff)));
break;
}
}
}
}
if (! validator.validateResources(list.toArray(new IFile[list.size()]))) {
return;
}
final int WORK_UNIT= 10;
if (pm != null) {
String message= Messages.Patcher_0;
pm.beginTask(message, fDiffs.length*WORK_UNIT);
}
for (i= 0; i < fDiffs.length; i++) {
int workTicks= WORK_UNIT;
FilePatch2 diff= fDiffs[i];
if (isEnabled(diff)) {
IPath path= getPath(diff);
if (pm != null)
pm.subTask(path.toString());
IFile file= singleFile != null
? singleFile
: createPath(container, path);
List<Hunk> failed= new ArrayList<>();
int type= diff.getDiffType(isReversed());
switch (type) {
case FilePatch2.ADDITION:
// patch it and collect rejected hunks
List<String> result= apply(diff, file, true, failed);
if (result != null)
store(LineReader.createString(isPreserveLineDelimeters(), result), file, SubMonitor.convert(pm, workTicks));
workTicks-= WORK_UNIT;
break;
case FilePatch2.DELETION:
file.delete(true, true, SubMonitor.convert(pm, workTicks));
workTicks-= WORK_UNIT;
break;
case FilePatch2.CHANGE:
// patch it and collect rejected hunks
result= apply(diff, file, false, failed);
if (result != null)
store(LineReader.createString(isPreserveLineDelimeters(), result), file, SubMonitor.convert(pm, workTicks));
workTicks-= WORK_UNIT;
break;
}
if (isGenerateRejectFile() && failed.size() > 0) {
IPath pp = getRejectFilePath(path);
file= createPath(container, pp);
if (file != null) {
store(getRejected(failed), file, pm);
try {
IMarker marker= file.createMarker(MARKER_TYPE);
marker.setAttribute(IMarker.MESSAGE, Messages.Patcher_1);
marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
} catch (CoreException ex) {
// NeedWork
}
}
}
}
if (pm != null) {
if (pm.isCanceled())
break;
if (workTicks > 0)
pm.worked(workTicks);
}
}
}
private IPath getRejectFilePath(IPath path) {
IPath pp= null;
if (path.segmentCount() > 1) {
pp= path.removeLastSegments(1);
pp= pp.append(path.lastSegment() + REJECT_FILE_EXTENSION);
} else
pp= new Path(path.lastSegment() + REJECT_FILE_EXTENSION);
return pp;
}
List<String> apply(FilePatch2 diff, IFile file, boolean create, List<Hunk> failedHunks) {
FileDiffResult result = getDiffResult(diff);
List<String> lines = LineReader.load(file, create);
result.patch(lines, null);
failedHunks.addAll(result.getFailedHunks());
if (hasCachedContents(diff)) {
// Used the cached contents since they would have been provided by the user
return getCachedLines(diff);
} else if (!result.hasMatches()) {
// Return null if there were no matches
return null;
}
return result.getLines();
}
/*
* Converts the string into bytes and stores them in the given file.
*/
protected void store(String contents, IFile file, IProgressMonitor pm) throws CoreException {
if (!file.exists()) {
if (FileBuffers.getTextFileBufferManager().isTextFileLocation(file.getFullPath(), true)) {
// For new text files use the line delimiter as defined in the workspace
String expectedLD= getLineDelimiterPreference(file);
if (expectedLD != null) {
String patchLD= TextUtilities.determineLineDelimiter(contents, expectedLD);
if (!expectedLD.equals(patchLD))
contents= contents.replaceAll(patchLD, expectedLD);
}
}
}
byte[] bytes;
try {
bytes= contents.getBytes(Utilities.getCharset(file));
} catch (UnsupportedEncodingException x) {
// uses default encoding
bytes= contents.getBytes();
}
store(bytes,file, pm);
}
private static String getLineDelimiterPreference(IFile file) {
IScopeContext[] scopeContext;
if (file != null && file.getProject() != null) {
// project preference
scopeContext= new IScopeContext[] { new ProjectScope(file.getProject()) };
String lineDelimiter= Platform.getPreferencesService().getString(Platform.PI_RUNTIME, Platform.PREF_LINE_SEPARATOR, null, scopeContext);
if (lineDelimiter != null)
return lineDelimiter;
}
// workspace preference
scopeContext= new IScopeContext[] { InstanceScope.INSTANCE };
return Platform.getPreferencesService().getString(Platform.PI_RUNTIME, Platform.PREF_LINE_SEPARATOR, null, scopeContext);
}
protected void store(byte[] bytes, IFile file, IProgressMonitor pm) throws CoreException {
InputStream is= new ByteArrayInputStream(bytes);
try {
if (file.exists()) {
file.setContents(is, false, true, pm);
} else {
file.create(is, false, pm);
}
} finally {
if (is != null)
try {
is.close();
} catch(IOException ex) {
// silently ignored
}
}
}
public boolean isPreserveLineDelimeters() {
return true;
}
public static String getRejected(List<Hunk> failedHunks) {
if (failedHunks.size() <= 0)
return null;
String lineSeparator= System.getProperty("line.separator"); //$NON-NLS-1$
StringBuilder sb= new StringBuilder();
Iterator<Hunk> iter= failedHunks.iterator();
while (iter.hasNext()) {
Hunk hunk= iter.next();
sb.append(hunk.getRejectedDescription());
sb.append(lineSeparator);
sb.append(hunk.getContent());
}
return sb.toString();
}
/*
* Ensures that a file with the given path exists in
* the given container. Folder are created as necessary.
*/
protected IFile createPath(IContainer container, IPath path) throws CoreException {
if (path.segmentCount() > 1) {
IContainer childContainer;
if (container instanceof IWorkspaceRoot) {
IProject project = ((IWorkspaceRoot)container).getProject(path.segment(0));
if (!project.exists())
project.create(null);
if (!project.isOpen())
project.open(null);
childContainer = project;
} else {
IFolder f= container.getFolder(path.uptoSegment(1));
if (!f.exists())
f.create(false, true, null);
childContainer = f;
}
return createPath(childContainer, path.removeFirstSegments(1));
}
// a leaf
return container.getFile(path);
}
public IResource getTarget() {
return fTarget;
}
public void setTarget(IResource target) {
fTarget= target;
}
public IFile getTargetFile(FilePatch2 diff) {
IPath path = diff.getStrippedPath(getStripPrefixSegments(), isReversed());
return existsInTarget(path);
}
/**
* Iterates through all of the resources contained in the Patch Wizard target
* and looks to for a match to the passed in file
* @param path
* @return IFile which matches the passed in path or null if none found
*/
public IFile existsInTarget(IPath path) {
if (fTarget instanceof IFile) { // special case
IFile file= (IFile) fTarget;
if (matches(file.getFullPath(), path))
return file;
} else if (fTarget instanceof IContainer) {
IContainer c= (IContainer) fTarget;
if (c.exists(path))
return c.getFile(path);
}
return null;
}
/**
* Returns true if path completely matches the end of fullpath
* @param fullpath
* @param path
* @return true if path matches, false otherwise
*/
private boolean matches(IPath fullpath, IPath path) {
for (IPath p= fullpath; path.segmentCount()<=p.segmentCount(); p= p.removeFirstSegments(1)) {
if (p.equals(path))
return true;
}
return false;
}
public int calculatePrefixSegmentCount() {
//Update prefix count - go through all of the diffs and find the smallest
//path segment contained in all diffs.
int length= 99;
if (fDiffs!=null) {
for (FilePatch2 diff : fDiffs) {
length= Math.min(length, diff.segmentCount());
}
if (ResourcesPlugin.getWorkspace().getRoot().equals(fTarget))
length--;
}
return length;
}
public void addDiff(FilePatch2 newDiff){
FilePatch2[] temp = new FilePatch2[fDiffs.length + 1];
System.arraycopy(fDiffs,0, temp, 0, fDiffs.length);
temp[fDiffs.length] = newDiff;
fDiffs = temp;
}
public void removeDiff(FilePatch2 diffToRemove){
FilePatch2[] temp = new FilePatch2[fDiffs.length - 1];
int counter = 0;
for (FilePatch2 diff : fDiffs) {
if (diff != diffToRemove) {
temp[counter++] = diff;
}
}
fDiffs = temp;
}
public void setEnabled(Object element, boolean enabled) {
if (element instanceof DiffProject)
setEnabledProject((DiffProject) element, enabled);
if (element instanceof FilePatch2)
setEnabledFile((FilePatch2)element, enabled);
if (element instanceof Hunk)
setEnabledHunk((Hunk) element, enabled);
}
private void setEnabledProject(DiffProject projectDiff, boolean enabled) {
FilePatch2[] diffFiles = projectDiff.getFileDiffs();
for (FilePatch2 diffFile : diffFiles) {
setEnabledFile(diffFile, enabled);
}
}
private void setEnabledFile(FilePatch2 fileDiff, boolean enabled) {
IHunk[] hunks = fileDiff.getHunks();
for (IHunk hunk : hunks) {
setEnabledHunk((Hunk) hunk, enabled);
}
}
private void setEnabledHunk(Hunk hunk, boolean enabled) {
if (enabled) {
disabledElements.remove(hunk);
FilePatch2 file = hunk.getParent();
disabledElements.remove(file);
DiffProject project = file.getProject();
if (project != null)
disabledElements.remove(project);
} else {
disabledElements.add(hunk);
FilePatch2 file = hunk.getParent();
if (disabledElements.containsAll(Arrays.asList(file.getHunks()))) {
disabledElements.add(file);
DiffProject project = file.getProject();
if (project != null
&& disabledElements.containsAll(Arrays.asList(project
.getFileDiffs())))
disabledElements.add(project);
}
}
}
public boolean isEnabled(Object element) {
if (disabledElements.contains(element))
return false;
Object parent = getElementParent(element);
if (parent == null)
return true;
return isEnabled(parent);
}
protected Object getElementParent(Object element) {
if (element instanceof Hunk) {
Hunk hunk = (Hunk) element;
return hunk.getParent();
}
return null;
}
/**
* Calculate the fuzz factor that will allow the most hunks to be matched.
* @param monitor a progress monitor
* @return the fuzz factor or <code>-1</code> if no hunks could be matched
*/
public int guessFuzzFactor(IProgressMonitor monitor) {
try {
monitor.beginTask(Messages.Patcher_2, IProgressMonitor.UNKNOWN);
FilePatch2[] diffs= getDiffs();
if (diffs==null||diffs.length<=0)
return -1;
int fuzz= -1;
for (FilePatch2 d : diffs) {
IFile file= getTargetFile(d);
if (file != null && file.exists()) {
List<String> lines= LineReader.load(file, false);
FileDiffResult result = getDiffResult(d);
int f = result.calculateFuzz(lines, monitor);
if (f > fuzz)
fuzz = f;
}
}
return fuzz;
} finally {
monitor.done();
}
}
public void refresh() {
diffResults.clear();
refresh(getDiffs());
}
public void refresh(FilePatch2[] diffs) {
for (FilePatch2 diff : diffs) {
FileDiffResult result = getDiffResult(diff);
((WorkspaceFileDiffResult)result).refresh();
}
}
public FileDiffResult getDiffResult(FilePatch2 diff) {
FileDiffResult result = diffResults.get(diff);
if (result == null) {
result = new WorkspaceFileDiffResult(diff, getConfiguration());
diffResults.put(diff, result);
}
return result;
}
public PatchConfiguration getConfiguration() {
return configuration;
}
/**
* Return the project that contains this diff or <code>null</code>
* if the patch is not a workspace patch.
* @param diff the diff
* @return the project that contains the diff
*/
public DiffProject getProject(FilePatch2 diff) {
return diff.getProject();
}
/*
* Returns <code>true</code> if new value differs from old.
*/
public boolean setReversed(boolean reverse) {
if (getConfiguration().isReversed() != reverse) {
getConfiguration().setReversed(reverse);
refresh();
return true;
}
return false;
}
public boolean isReversed() {
return getConfiguration().isReversed();
}
/**
* Cache the contents for the given file diff. These contents
* will be used for the diff when the patch is applied. When the
* patch is applied, it is assumed that the provided contents
* already have all relevant hunks applied.
* @param diff the file diff
* @param contents the contents for the file diff
*/
public void cacheContents(FilePatch2 diff, byte[] contents) {
contentCache.put(diff, contents);
}
/**
* Return whether contents have been cached for the
* given file diff.
* @param diff the file diff
* @return whether contents have been cached for the file diff
* @see #cacheContents(FilePatch2, byte[])
*/
public boolean hasCachedContents(FilePatch2 diff) {
return contentCache.containsKey(diff);
}
/**
* Return the content lines that are cached for the given
* file diff.
* @param diff the file diff
* @return the content lines that are cached for the file diff
*/
public List<String> getCachedLines(FilePatch2 diff) {
byte[] contents = contentCache.get(diff);
if (contents != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(contents)));
return LineReader.readLines(reader);
}
return null;
}
/**
* Return the contents that are cached for the given diff or
* <code>null</code> if there is no contents cached.
* @param diff the diff
* @return the contents that are cached for the given diff or
* <code>null</code>
*/
public byte[] getCachedContents(FilePatch2 diff) {
return contentCache.get(diff);
}
/**
* Return whether the patcher has any cached contents.
* @return whether the patcher has any cached contents
*/
public boolean hasCachedContents() {
return !contentCache.isEmpty();
}
/**
* Clear any cached contents.
*/
public void clearCachedContents() {
contentCache.clear();
mergedHunks.clear();
}
public void setProperty(String key, Object value) {
getConfiguration().setProperty(key, value);
}
public Object getProperty(String key) {
return getConfiguration().getProperty(key);
}
public boolean isManuallyMerged(Hunk hunk) {
return mergedHunks.contains(hunk);
}
public void setManuallyMerged(Hunk hunk, boolean merged) {
if (merged)
mergedHunks.add(hunk);
else
mergedHunks.remove(hunk);
}
public IProject getTargetProject(FilePatch2 diff) {
DiffProject dp = getProject(diff);
if (dp != null)
return Utilities.getProject(dp);
IResource tr = getTarget();
if (tr instanceof IWorkspaceRoot) {
IWorkspaceRoot root = (IWorkspaceRoot) tr;
return root.getProject(diff.getPath(isReversed()).segment(0));
}
return tr.getProject();
}
public static Patcher getPatcher(PatchConfiguration configuration) {
return (Patcher)configuration.getProperty(PROP_PATCHER);
}
public boolean hasRejects() {
for (FileDiffResult result : diffResults.values()) {
if (result.hasRejects())
return true;
}
return false;
}
@Override
public boolean select(IHunk hunk) {
return isEnabled(hunk);
}
}