| // 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; | |
| } | |
| } | |
| } |