blob: cbf110a9fe9dd732275638772901e00447882807 [file] [log] [blame]
/*
* Copyright 2000-2010 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;
import com.intellij.openapi.diff.impl.string.DiffString;
import com.intellij.openapi.diff.ex.DiffFragment;
import com.intellij.openapi.diff.impl.ComparisonPolicy;
import com.intellij.openapi.diff.impl.fragments.LineFragment;
import com.intellij.openapi.diff.impl.processing.DiffCorrection;
import com.intellij.openapi.diff.impl.processing.DiffFragmentsProcessor;
import com.intellij.openapi.diff.impl.processing.DiffPolicy;
import com.intellij.openapi.diff.impl.util.TextDiffTypeEnum;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.util.BeforeAfter;
import com.intellij.util.diff.FilesTooBigForDiffException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author yole
*/
public class TextPatchBuilder {
private static final int CONTEXT_LINES = 3;
@NonNls private static final String REVISION_NAME_TEMPLATE = "(revision {0})";
@NonNls private static final String DATE_NAME_TEMPLATE = "(date {0})";
private final String myBasePath;
private final boolean myIsReversePath;
private final boolean myIsCaseSensitive;
@Nullable
private final Runnable myCancelChecker;
private final boolean myIncludeBaseText;
private TextPatchBuilder(final String basePath, final boolean isReversePath, final boolean isCaseSensitive,
@Nullable final Runnable cancelChecker, boolean includeBaseText) {
myBasePath = basePath;
myIsReversePath = isReversePath;
myIsCaseSensitive = isCaseSensitive;
myCancelChecker = cancelChecker;
myIncludeBaseText = includeBaseText;
}
private void checkCanceled() {
if (myCancelChecker != null) {
myCancelChecker.run();
}
}
public static List<FilePatch> buildPatch(final Collection<BeforeAfter<AirContentRevision>> changes, final String basePath,
final boolean reversePatch,
final boolean isCaseSensitive,
@Nullable final Runnable cancelChecker,
final boolean includeBaseText) throws VcsException {
final TextPatchBuilder builder = new TextPatchBuilder(basePath, reversePatch, isCaseSensitive, cancelChecker, includeBaseText);
return builder.build(changes);
}
private List<FilePatch> build(final Collection<BeforeAfter<AirContentRevision>> changes) throws VcsException {
List<FilePatch> result = new ArrayList<FilePatch>();
for(BeforeAfter<AirContentRevision> c: changes) {
checkCanceled();
final AirContentRevision beforeRevision;
final AirContentRevision afterRevision;
if (myIsReversePath) {
beforeRevision = c.getAfter();
afterRevision = c.getBefore();
}
else {
beforeRevision = c.getBefore();
afterRevision = c.getAfter();
}
if (beforeRevision != null && beforeRevision.getPath().isDirectory()) {
continue;
}
if (afterRevision != null && afterRevision.getPath().isDirectory()) {
continue;
}
if ((beforeRevision != null) && beforeRevision.isBinary() || (afterRevision != null) && afterRevision.isBinary()) {
result.add(buildBinaryPatch(myBasePath, beforeRevision, afterRevision));
continue;
}
if (beforeRevision == null) {
result.add(buildAddedFile(myBasePath, afterRevision));
continue;
}
if (afterRevision == null) {
result.add(buildDeletedFile(myBasePath, beforeRevision));
continue;
}
final DiffString beforeContent = DiffString.createNullable(beforeRevision.getContentAsString());
if (beforeContent == null) {
throw new VcsException("Failed to fetch old content for changed file " + beforeRevision.getPath().getPath());
}
final DiffString afterContent = DiffString.createNullable(afterRevision.getContentAsString());
if (afterContent == null) {
throw new VcsException("Failed to fetch new content for changed file " + afterRevision.getPath().getPath());
}
DiffString[] beforeLines = tokenize(beforeContent);
DiffString[] afterLines = tokenize(afterContent);
DiffFragment[] woFormattingBlocks;
DiffFragment[] step1lineFragments;
try {
woFormattingBlocks = DiffPolicy.LINES_WO_FORMATTING.buildFragments(beforeContent, afterContent);
step1lineFragments = new DiffCorrection.TrueLineBlocks(ComparisonPolicy.DEFAULT).correctAndNormalize(woFormattingBlocks);
}
catch (FilesTooBigForDiffException e) {
throw new VcsException("File '" + myBasePath + "' is too big and there are too many changes to build diff", e);
}
ArrayList<LineFragment> fragments = new DiffFragmentsProcessor().process(step1lineFragments);
if (fragments.size() > 1 || (fragments.size() == 1 && fragments.get(0).getType() != null && fragments.get(0).getType() != TextDiffTypeEnum.NONE)) {
TextFilePatch patch = buildPatchHeading(myBasePath, beforeRevision, afterRevision);
result.add(patch);
int lastLine1 = 0;
int lastLine2 = 0;
while(fragments.size() > 0) {
checkCanceled();
List<LineFragment> adjacentFragments = getAdjacentFragments(fragments);
if (adjacentFragments.size() > 0) {
LineFragment first = adjacentFragments.get(0);
LineFragment last = adjacentFragments.get(adjacentFragments.size()-1);
final int start1 = first.getStartingLine1();
final int start2 = first.getStartingLine2();
final int end1 = last.getStartingLine1() + last.getModifiedLines1();
final int end2 = last.getStartingLine2() + last.getModifiedLines2();
int contextStart1 = Math.max(start1 - CONTEXT_LINES, lastLine1);
int contextStart2 = Math.max(start2 - CONTEXT_LINES, lastLine2);
int contextEnd1 = Math.min(end1 + CONTEXT_LINES, beforeLines.length);
int contextEnd2 = Math.min(end2 + CONTEXT_LINES, afterLines.length);
PatchHunk hunk = new PatchHunk(contextStart1, contextEnd1, contextStart2, contextEnd2);
patch.addHunk(hunk);
for(LineFragment fragment: adjacentFragments) {
checkCanceled();
for(int i=contextStart1; i<fragment.getStartingLine1(); i++) {
addLineToHunk(hunk, beforeLines[i], PatchLine.Type.CONTEXT);
}
for(int i=fragment.getStartingLine1(); i<fragment.getStartingLine1()+fragment.getModifiedLines1(); i++) {
addLineToHunk(hunk, beforeLines[i], PatchLine.Type.REMOVE);
}
for(int i=fragment.getStartingLine2(); i<fragment.getStartingLine2()+fragment.getModifiedLines2(); i++) {
addLineToHunk(hunk, afterLines[i], PatchLine.Type.ADD);
}
contextStart1 = fragment.getStartingLine1()+fragment.getModifiedLines1();
}
for(int i=contextStart1; i<contextEnd1; i++) {
addLineToHunk(hunk, beforeLines[i], PatchLine.Type.CONTEXT);
}
}
}
checkPathEndLine(patch, c.getAfter());
} else if (! beforeRevision.getPath().equals(afterRevision.getPath())) {
final TextFilePatch movedPatch = buildMovedFile(myBasePath, beforeRevision, afterRevision);
checkPathEndLine(movedPatch, c.getAfter());
result.add(movedPatch);
}
}
return result;
}
private void checkPathEndLine(TextFilePatch filePatch, final AirContentRevision cr) throws VcsException {
if (cr == null) return;
if (filePatch.isDeletedFile() || filePatch.getAfterName() == null) return;
final List<PatchHunk> hunks = filePatch.getHunks();
if (hunks.isEmpty()) return;
final PatchHunk hunk = hunks.get(hunks.size() - 1);
final List<PatchLine> lines = hunk.getLines();
if (lines.isEmpty()) return;
final String contentAsString = cr.getContentAsString();
if (contentAsString == null) return;
if (! contentAsString.endsWith("\n")) {
lines.get(lines.size() - 1).setSuppressNewLine(true);
}
}
@NotNull
private static DiffString[] tokenize(@NotNull DiffString text) {
return text.length() == 0 ? new DiffString[]{text} : text.tokenize();
}
private FilePatch buildBinaryPatch(final String basePath,
final AirContentRevision beforeRevision,
final AirContentRevision afterRevision) throws VcsException {
AirContentRevision headingBeforeRevision = beforeRevision != null ? beforeRevision : afterRevision;
AirContentRevision headingAfterRevision = afterRevision != null ? afterRevision : beforeRevision;
byte[] beforeContent = beforeRevision != null ? beforeRevision.getContentAsBytes() : null;
byte[] afterContent = afterRevision != null ? afterRevision.getContentAsBytes() : null;
BinaryFilePatch patch = new BinaryFilePatch(beforeContent, afterContent);
setPatchHeading(patch, basePath, headingBeforeRevision, headingAfterRevision);
return patch;
}
private static void addLineToHunk(@NotNull final PatchHunk hunk, @NotNull final DiffString line, final PatchLine.Type type) {
final PatchLine patchLine;
if (!line.endsWith('\n')) {
patchLine = new PatchLine(type, line.toString());
patchLine.setSuppressNewLine(true);
}
else {
patchLine = new PatchLine(type, line.substring(0, line.length() - 1).toString());
}
hunk.addLine(patchLine);
}
private TextFilePatch buildMovedFile(final String basePath, final AirContentRevision beforeRevision,
final AirContentRevision afterRevision) throws VcsException {
final TextFilePatch result = buildPatchHeading(basePath, beforeRevision, afterRevision);
final PatchHunk hunk = new PatchHunk(0, 0, 0, 0);
result.addHunk(hunk);
return result;
}
private TextFilePatch buildAddedFile(final String basePath, final AirContentRevision afterRevision) throws VcsException {
final DiffString content = DiffString.createNullable(afterRevision.getContentAsString());
if (content == null) {
throw new VcsException("Failed to fetch content for added file " + afterRevision.getPath().getPath());
}
DiffString[] lines = tokenize(content);
TextFilePatch result = buildPatchHeading(basePath, afterRevision, afterRevision);
PatchHunk hunk = new PatchHunk(-1, -1, 0, lines.length);
for (DiffString line : lines) {
checkCanceled();
addLineToHunk(hunk, line, PatchLine.Type.ADD);
}
result.addHunk(hunk);
return result;
}
private TextFilePatch buildDeletedFile(String basePath, AirContentRevision beforeRevision) throws VcsException {
final DiffString content = DiffString.createNullable(beforeRevision.getContentAsString());
if (content == null) {
throw new VcsException("Failed to fetch old content for deleted file " + beforeRevision.getPath().getPath());
}
DiffString[] lines = tokenize(content);
TextFilePatch result = buildPatchHeading(basePath, beforeRevision, beforeRevision);
PatchHunk hunk = new PatchHunk(0, lines.length, -1, -1);
for (DiffString line : lines) {
checkCanceled();
addLineToHunk(hunk, line, PatchLine.Type.REMOVE);
}
result.addHunk(hunk);
return result;
}
private static List<LineFragment> getAdjacentFragments(final ArrayList<LineFragment> fragments) {
List<LineFragment> result = new ArrayList<LineFragment>();
int endLine = -1;
while(!fragments.isEmpty()) {
LineFragment fragment = fragments.get(0);
if (fragment.getType() == null || fragment.getType() == TextDiffTypeEnum.NONE) {
fragments.remove(0);
continue;
}
if (result.isEmpty() || endLine + CONTEXT_LINES >= fragment.getStartingLine1() - CONTEXT_LINES) {
result.add(fragment);
fragments.remove(0);
endLine = fragment.getStartingLine1() + fragment.getModifiedLines1();
}
else {
break;
}
}
return result;
}
private String getRelativePath(final String basePath, final String secondPath) {
final String baseModified = FileUtil.toSystemIndependentName(basePath);
final String secondModified = FileUtil.toSystemIndependentName(secondPath);
final String relPath = FileUtil.getRelativePath(baseModified, secondModified, '/', myIsCaseSensitive);
if (relPath == null) return secondModified;
return relPath;
}
private static String getRevisionName(final AirContentRevision revision) {
final String revisionName = revision.getRevisionNumber();
if (revisionName != null) {
return MessageFormat.format(REVISION_NAME_TEMPLATE, revisionName);
}
return MessageFormat.format(DATE_NAME_TEMPLATE, Long.toString(revision.getPath().lastModified()));
}
private TextFilePatch buildPatchHeading(final String basePath, final AirContentRevision beforeRevision, final AirContentRevision afterRevision) {
TextFilePatch result = new TextFilePatch(afterRevision == null ? null : afterRevision.getCharset());
setPatchHeading(result, basePath, beforeRevision, afterRevision);
return result;
}
private void setPatchHeading(final FilePatch result, final String basePath,
@NotNull final AirContentRevision beforeRevision,
@NotNull final AirContentRevision afterRevision) {
result.setBeforeName(getRelativePath(basePath, beforeRevision.getPath().getPath()));
result.setBeforeVersionId(getRevisionName(beforeRevision));
result.setAfterName(getRelativePath(basePath, afterRevision.getPath().getPath()));
result.setAfterVersionId(getRevisionName(afterRevision));
}
}