blob: 606e42b19b92cc1912f8156f1467ca9e904e5a4d [file] [log] [blame]
// Copyright (C) 2019 The Android Open Source Project
//
// 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.google.gerrit.server.diff;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.common.data.PatchScript.PatchScriptFileInfo;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus;
import com.google.gerrit.extensions.common.DiffWebLinkInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
import com.google.gerrit.jgit.diff.ReplaceEdit;
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.server.change.FileContentUtil;
import com.google.gerrit.server.project.ProjectState;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.diff.Edit;
/** Creates and fills a new {@link DiffInfo} object based on diff between files. */
public class DiffInfoCreator {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
Maps.immutableEnumMap(
new ImmutableMap.Builder<Patch.ChangeType, ChangeType>()
.put(Patch.ChangeType.ADDED, ChangeType.ADDED)
.put(Patch.ChangeType.MODIFIED, ChangeType.MODIFIED)
.put(Patch.ChangeType.DELETED, ChangeType.DELETED)
.put(Patch.ChangeType.RENAMED, ChangeType.RENAMED)
.put(Patch.ChangeType.COPIED, ChangeType.COPIED)
.put(Patch.ChangeType.REWRITE, ChangeType.REWRITE)
.build());
private final DiffWebLinksProvider webLinksProvider;
private final boolean intraline;
private final ProjectState state;
public DiffInfoCreator(
ProjectState state, DiffWebLinksProvider webLinksProvider, boolean intraline) {
this.webLinksProvider = webLinksProvider;
this.state = state;
this.intraline = intraline;
}
/* Returns the {@link DiffInfo} to display for end-users */
public DiffInfo create(PatchScript ps, DiffSide sideA, DiffSide sideB) {
DiffInfo result = new DiffInfo();
ImmutableList<DiffWebLinkInfo> links = webLinksProvider.getDiffLinks();
result.webLinks = links.isEmpty() ? null : links;
ImmutableList<WebLinkInfo> editLinks = webLinksProvider.getEditWebLinks();
result.editWebLinks = editLinks.isEmpty() ? null : editLinks;
if (ps.isBinary()) {
result.binary = true;
}
result.metaA = createFileMeta(sideA).orElse(null);
result.metaB = createFileMeta(sideB).orElse(null);
if (intraline) {
if (ps.hasIntralineTimeout()) {
result.intralineStatus = IntraLineStatus.TIMEOUT;
} else if (ps.hasIntralineFailure()) {
result.intralineStatus = IntraLineStatus.FAILURE;
} else {
result.intralineStatus = IntraLineStatus.OK;
}
logger.atFine().log("intralineStatus = %s", result.intralineStatus);
}
result.changeType = CHANGE_TYPE.get(ps.getChangeType());
logger.atFine().log("changeType = %s", result.changeType);
if (result.changeType == null) {
throw new IllegalStateException("unknown change type: " + ps.getChangeType());
}
if (ps.getPatchHeader().size() > 0) {
result.diffHeader = ps.getPatchHeader();
}
result.content = calculateDiffContentEntries(ps);
return result;
}
private static List<ContentEntry> calculateDiffContentEntries(PatchScript ps) {
ContentCollector contentCollector = new ContentCollector(ps);
Set<Edit> editsDueToRebase = ps.getEditsDueToRebase();
for (Edit edit : ps.getEdits()) {
logger.atFine().log("next edit = %s", edit);
if (edit.getType() == Edit.Type.EMPTY) {
logger.atFine().log("skip empty edit");
continue;
}
contentCollector.addCommon(edit.getBeginA());
checkState(
contentCollector.nextA == edit.getBeginA(),
"nextA = %s; want %s",
contentCollector.nextA,
edit.getBeginA());
checkState(
contentCollector.nextB == edit.getBeginB(),
"nextB = %s; want %s",
contentCollector.nextB,
edit.getBeginB());
switch (edit.getType()) {
case DELETE:
case INSERT:
case REPLACE:
List<Edit> internalEdit =
edit instanceof ReplaceEdit ? ((ReplaceEdit) edit).getInternalEdits() : null;
boolean dueToRebase = editsDueToRebase.contains(edit);
contentCollector.addDiff(edit.getEndA(), edit.getEndB(), internalEdit, dueToRebase);
break;
case EMPTY:
default:
throw new IllegalStateException();
}
}
contentCollector.addCommon(ps.getA().getSize());
return contentCollector.lines;
}
private Optional<FileMeta> createFileMeta(DiffSide side) {
PatchScriptFileInfo fileInfo = side.fileInfo();
if (fileInfo.displayMethod == DisplayMethod.NONE) {
return Optional.empty();
}
FileMeta result = new FileMeta();
result.name = side.fileName();
result.contentType =
FileContentUtil.resolveContentType(
state, side.fileName(), fileInfo.mode, fileInfo.mimeType);
result.lines = fileInfo.content.getSize();
ImmutableList<WebLinkInfo> fileLinks = webLinksProvider.getFileWebLinks(side.type());
result.webLinks = fileLinks.isEmpty() ? null : fileLinks;
result.commitId = fileInfo.commitId;
return Optional.of(result);
}
private static class ContentCollector {
private final List<ContentEntry> lines;
private final SparseFileContent.Accessor fileA;
private final SparseFileContent.Accessor fileB;
private final boolean ignoreWS;
private int nextA;
private int nextB;
ContentCollector(PatchScript ps) {
lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2);
fileA = ps.getA().createAccessor();
fileB = ps.getB().createAccessor();
ignoreWS = ps.isIgnoreWhitespace();
}
void addCommon(int end) {
logger.atFine().log("addCommon: end = %d", end);
end = Math.min(end, fileA.getSize());
logger.atFine().log("end = %d", end);
if (nextA >= end) {
logger.atFine().log("nextA >= end: nextA = %d, end = %d", nextA, end);
return;
}
while (nextA < end) {
logger.atFine().log("nextA < end: nextA = %d, end = %d", nextA, end);
if (!fileA.contains(nextA)) {
logger.atFine().log("fileA does not contain nextA: nextA = %d", nextA);
int endRegion = Math.min(end, nextA == 0 ? fileA.first() : fileA.next(nextA - 1));
int len = endRegion - nextA;
entry().skip = len;
nextA = endRegion;
nextB += len;
logger.atFine().log("setting: nextA = %d, nextB = %d", nextA, nextB);
continue;
}
ContentEntry e = null;
for (int i = nextA; i == nextA && i < end; i = fileA.next(i), nextA++, nextB++) {
if (ignoreWS && fileB.contains(nextB)) {
if (e == null || e.common == null) {
logger.atFine().log("create new common entry: nextA = %d, nextB = %d", nextA, nextB);
e = entry();
e.a = Lists.newArrayListWithCapacity(end - nextA);
e.b = Lists.newArrayListWithCapacity(end - nextA);
e.common = true;
}
e.a.add(fileA.get(nextA));
e.b.add(fileB.get(nextB));
} else {
if (e == null || e.common != null) {
logger.atFine().log(
"create new non-common entry: nextA = %d, nextB = %d", nextA, nextB);
e = entry();
e.ab = Lists.newArrayListWithCapacity(end - nextA);
}
e.ab.add(fileA.get(nextA));
}
}
}
}
void addDiff(int endA, int endB, List<Edit> internalEdit, boolean dueToRebase) {
logger.atFine().log(
"addDiff: endA = %d, endB = %d, numberOfInternalEdits = %d, dueToRebase = %s",
endA, endB, internalEdit != null ? internalEdit.size() : 0, dueToRebase);
int lenA = endA - nextA;
int lenB = endB - nextB;
logger.atFine().log("lenA = %d, lenB = %d", lenA, lenB);
checkState(lenA > 0 || lenB > 0);
logger.atFine().log("create non-common entry");
ContentEntry e = entry();
if (lenA > 0) {
logger.atFine().log("lenA > 0: lenA = %d", lenA);
e.a = Lists.newArrayListWithCapacity(lenA);
for (; nextA < endA; nextA++) {
e.a.add(fileA.get(nextA));
}
}
if (lenB > 0) {
logger.atFine().log("lenB > 0: lenB = %d", lenB);
e.b = Lists.newArrayListWithCapacity(lenB);
for (; nextB < endB; nextB++) {
e.b.add(fileB.get(nextB));
}
}
if (internalEdit != null && !internalEdit.isEmpty()) {
logger.atFine().log("processing internal edits");
e.editA = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
e.editB = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
int lastA = 0;
int lastB = 0;
for (Edit edit : internalEdit) {
logger.atFine().log("internal edit = %s", edit);
if (edit.getBeginA() != edit.getEndA()) {
logger.atFine().log(
"edit.getBeginA() != edit.getEndA(): edit.getBeginA() = %d, edit.getEndA() = %d",
edit.getBeginA(), edit.getEndA());
e.editA.add(
ImmutableList.of(edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
lastA = edit.getEndA();
logger.atFine().log("lastA = %d", lastA);
}
if (edit.getBeginB() != edit.getEndB()) {
logger.atFine().log(
"edit.getBeginB() != edit.getEndB(): edit.getBeginB() = %d, edit.getEndB() = %d",
edit.getBeginB(), edit.getEndB());
e.editB.add(
ImmutableList.of(edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
lastB = edit.getEndB();
logger.atFine().log("lastB = %d", lastB);
}
}
}
e.dueToRebase = dueToRebase ? true : null;
}
private ContentEntry entry() {
ContentEntry e = new ContentEntry();
lines.add(e);
return e;
}
}
}