diff options
Diffstat (limited to 'java/com/google/gerrit/server/patch/PatchScriptBuilder.java')
-rw-r--r-- | java/com/google/gerrit/server/patch/PatchScriptBuilder.java | 620 |
1 files changed, 620 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java new file mode 100644 index 0000000000..61f0180879 --- /dev/null +++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java @@ -0,0 +1,620 @@ +// Copyright (C) 2009 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.patch; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Comparator.comparing; + +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.common.data.CommentDetail; +import com.google.gerrit.common.data.PatchScript; +import com.google.gerrit.common.data.PatchScript.DisplayMethod; +import com.google.gerrit.extensions.client.DiffPreferencesInfo; +import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; +import com.google.gerrit.prettify.common.EditList; +import com.google.gerrit.prettify.common.SparseFileContent; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Comment; +import com.google.gerrit.reviewdb.client.Patch; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.mime.FileTypeRegistry; +import com.google.inject.Inject; +import eu.medsea.mimeutil.MimeType; +import eu.medsea.mimeutil.MimeUtil2; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import org.eclipse.jgit.diff.Edit; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; + +class PatchScriptBuilder { + static final int MAX_CONTEXT = 5000000; + static final int BIG_FILE = 9000; + + private static final Comparator<Edit> EDIT_SORT = comparing(Edit::getBeginA); + + private Repository db; + private Project.NameKey projectKey; + private ObjectReader reader; + private Change change; + private DiffPreferencesInfo diffPrefs; + private ComparisonType comparisonType; + private ObjectId aId; + private ObjectId bId; + + private final Side a; + private final Side b; + + private List<Edit> edits; + private final FileTypeRegistry registry; + private final PatchListCache patchListCache; + private int context; + + @Inject + PatchScriptBuilder(FileTypeRegistry ftr, PatchListCache plc) { + a = new Side(); + b = new Side(); + registry = ftr; + patchListCache = plc; + } + + void setRepository(Repository r, Project.NameKey projectKey) { + this.db = r; + this.projectKey = projectKey; + } + + void setChange(Change c) { + this.change = c; + } + + void setDiffPrefs(DiffPreferencesInfo dp) { + diffPrefs = dp; + + context = diffPrefs.context; + if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) { + context = MAX_CONTEXT; + } else if (context > MAX_CONTEXT) { + context = MAX_CONTEXT; + } + } + + void setTrees(ComparisonType ct, ObjectId a, ObjectId b) { + comparisonType = ct; + aId = a; + bId = b; + } + + PatchScript toPatchScript(PatchListEntry content, CommentDetail comments, List<Patch> history) + throws IOException { + reader = db.newObjectReader(); + try { + return build(content, comments, history); + } finally { + reader.close(); + } + } + + private PatchScript build(PatchListEntry content, CommentDetail comments, List<Patch> history) + throws IOException { + boolean intralineDifferenceIsPossible = true; + boolean intralineFailure = false; + boolean intralineTimeout = false; + + a.path = oldName(content); + b.path = newName(content); + + a.resolve(null, aId); + b.resolve(a, bId); + + edits = new ArrayList<>(content.getEdits()); + ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase(); + + if (!isModify(content)) { + intralineDifferenceIsPossible = false; + } else if (diffPrefs.intralineDifference) { + IntraLineDiff d = + patchListCache.getIntraLineDiff( + IntraLineDiffKey.create(a.id, b.id, diffPrefs.ignoreWhitespace), + IntraLineDiffArgs.create( + a.src, b.src, edits, editsDueToRebase, projectKey, bId, b.path)); + if (d != null) { + switch (d.getStatus()) { + case EDIT_LIST: + edits = new ArrayList<>(d.getEdits()); + break; + + case DISABLED: + intralineDifferenceIsPossible = false; + break; + + case ERROR: + intralineDifferenceIsPossible = false; + intralineFailure = true; + break; + + case TIMEOUT: + intralineDifferenceIsPossible = false; + intralineTimeout = true; + break; + } + } else { + intralineDifferenceIsPossible = false; + intralineFailure = true; + } + } + + correctForDifferencesInNewlineAtEnd(); + + if (comments != null) { + ensureCommentsVisible(comments); + } + + boolean hugeFile = false; + if (a.src == b.src && a.size() <= context && content.getEdits().isEmpty()) { + // Odd special case; the files are identical (100% rename or copy) + // and the user has asked for context that is larger than the file. + // Send them the entire file, with an empty edit after the last line. + // + for (int i = 0; i < a.size(); i++) { + a.addLine(i); + } + edits = new ArrayList<>(1); + edits.add(new Edit(a.size(), a.size())); + + } else { + if (BIG_FILE < Math.max(a.size(), b.size())) { + // IF the file is really large, we disable things to avoid choking + // the browser client. + // + hugeFile = true; + } + + // In order to expand the skipped common lines or syntax highlight the + // file properly we need to give the client the complete file contents. + // So force our context temporarily to the complete file size. + // + context = MAX_CONTEXT; + + packContent(diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE); + } + + return new PatchScript( + change.getKey(), + content.getChangeType(), + content.getOldName(), + content.getNewName(), + a.fileMode, + b.fileMode, + content.getHeaderLines(), + diffPrefs, + a.dst, + b.dst, + edits, + editsDueToRebase, + a.displayMethod, + b.displayMethod, + a.mimeType.toString(), + b.mimeType.toString(), + comments, + history, + hugeFile, + intralineDifferenceIsPossible, + intralineFailure, + intralineTimeout, + content.getPatchType() == Patch.PatchType.BINARY, + aId == null ? null : aId.getName(), + bId == null ? null : bId.getName()); + } + + private static boolean isModify(PatchListEntry content) { + switch (content.getChangeType()) { + case MODIFIED: + case COPIED: + case RENAMED: + case REWRITE: + return true; + + case ADDED: + case DELETED: + default: + return false; + } + } + + private static String oldName(PatchListEntry entry) { + switch (entry.getChangeType()) { + case ADDED: + return null; + case DELETED: + case MODIFIED: + case REWRITE: + return entry.getNewName(); + case COPIED: + case RENAMED: + default: + return entry.getOldName(); + } + } + + private static String newName(PatchListEntry entry) { + switch (entry.getChangeType()) { + case DELETED: + return null; + case ADDED: + case MODIFIED: + case COPIED: + case RENAMED: + case REWRITE: + default: + return entry.getNewName(); + } + } + + private void correctForDifferencesInNewlineAtEnd() { + // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it. + int aSize = a.src.size(); + int bSize = b.src.size(); + + if (edits.isEmpty() && (aSize == 0 || bSize == 0)) { + // The diff was requested for a file which was either added or deleted but which JGit doesn't + // consider a file addition/deletion (e.g. requesting a diff for the old file name of a + // renamed file looks like a deletion). + return; + } + + Optional<Edit> lastEdit = getLast(edits); + if (isNewlineAtEndDeleted()) { + Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize); + if (lastLineEdit.isPresent()) { + lastLineEdit.get().extendA(); + } else { + Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize); + edits.add(newlineEdit); + } + } else if (isNewlineAtEndAdded()) { + Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize); + if (lastLineEdit.isPresent()) { + lastLineEdit.get().extendB(); + } else { + Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1); + edits.add(newlineEdit); + } + } + } + + private static <T> Optional<T> getLast(List<T> list) { + return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1)); + } + + private boolean isNewlineAtEndDeleted() { + return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd(); + } + + private boolean isNewlineAtEndAdded() { + return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd(); + } + + private void ensureCommentsVisible(CommentDetail comments) { + if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) { + // No comments, no additional dummy edits are required. + // + return; + } + + // Construct empty Edit blocks around each location where a comment is. + // This will force the later packContent method to include the regions + // containing comments, potentially combining those regions together if + // they have overlapping contexts. UI renders will also be able to make + // correct hunks from this, but because the Edit is empty they will not + // style it specially. + // + final List<Edit> empty = new ArrayList<>(); + int lastLine; + + lastLine = -1; + for (Comment c : comments.getCommentsA()) { + final int a = c.lineNbr; + if (lastLine != a) { + final int b = mapA2B(a - 1); + if (0 <= b) { + safeAdd(empty, new Edit(a - 1, b)); + } + lastLine = a; + } + } + + lastLine = -1; + for (Comment c : comments.getCommentsB()) { + int b = c.lineNbr; + if (lastLine != b) { + final int a = mapB2A(b - 1); + if (0 <= a) { + safeAdd(empty, new Edit(a, b - 1)); + } + lastLine = b; + } + } + + // Sort the final list by the index in A, so packContent can combine + // them correctly later. + // + edits.addAll(empty); + edits.sort(EDIT_SORT); + } + + private void safeAdd(List<Edit> empty, Edit toAdd) { + final int a = toAdd.getBeginA(); + final int b = toAdd.getBeginB(); + for (Edit e : edits) { + if (e.getBeginA() <= a && a <= e.getEndA()) { + return; + } + if (e.getBeginB() <= b && b <= e.getEndB()) { + return; + } + } + empty.add(toAdd); + } + + private int mapA2B(int a) { + if (edits.isEmpty()) { + // Magic special case of an unmodified file. + // + return a; + } + + for (int i = 0; i < edits.size(); i++) { + final Edit e = edits.get(i); + if (a < e.getBeginA()) { + if (i == 0) { + // Special case of context at start of file. + // + return a; + } + return e.getBeginB() - (e.getBeginA() - a); + } + if (e.getBeginA() <= a && a <= e.getEndA()) { + return -1; + } + } + + final Edit last = edits.get(edits.size() - 1); + return last.getEndB() + (a - last.getEndA()); + } + + private int mapB2A(int b) { + if (edits.isEmpty()) { + // Magic special case of an unmodified file. + // + return b; + } + + for (int i = 0; i < edits.size(); i++) { + final Edit e = edits.get(i); + if (b < e.getBeginB()) { + if (i == 0) { + // Special case of context at start of file. + // + return b; + } + return e.getBeginA() - (e.getBeginB() - b); + } + if (e.getBeginB() <= b && b <= e.getEndB()) { + return -1; + } + } + + final Edit last = edits.get(edits.size() - 1); + return last.getEndA() + (b - last.getEndB()); + } + + private void packContent(boolean ignoredWhitespace) { + EditList list = new EditList(edits, context, a.size(), b.size()); + for (EditList.Hunk hunk : list.getHunks()) { + while (hunk.next()) { + if (hunk.isContextLine()) { + String lineA = a.getSourceLine(hunk.getCurA()); + a.dst.addLine(hunk.getCurA(), lineA); + + if (ignoredWhitespace) { + // If we ignored whitespace in some form, also get the line + // from b when it does not exactly match the line from a. + // + String lineB = b.getSourceLine(hunk.getCurB()); + if (!lineA.equals(lineB)) { + b.dst.addLine(hunk.getCurB(), lineB); + } + } + hunk.incBoth(); + continue; + } + + if (hunk.isDeletedA()) { + a.addLine(hunk.getCurA()); + hunk.incA(); + } + + if (hunk.isInsertedB()) { + b.addLine(hunk.getCurB()); + hunk.incB(); + } + } + } + } + + private class Side { + String path; + ObjectId id; + FileMode mode; + byte[] srcContent; + Text src; + MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE; + DisplayMethod displayMethod = DisplayMethod.DIFF; + PatchScript.FileMode fileMode = PatchScript.FileMode.FILE; + final SparseFileContent dst = new SparseFileContent(); + + int size() { + if (src == null) { + return 0; + } + if (src.isMissingNewlineAtEnd()) { + return src.size(); + } + return src.size() + 1; + } + + void addLine(int lineNumber) { + String lineContent = getSourceLine(lineNumber); + dst.addLine(lineNumber, lineContent); + } + + String getSourceLine(int lineNumber) { + return lineNumber >= src.size() ? "" : src.getString(lineNumber); + } + + void resolve(Side other, ObjectId within) throws IOException { + try { + final boolean reuse; + if (Patch.COMMIT_MSG.equals(path)) { + if (comparisonType.isAgainstParentOrAutoMerge() + && (aId == within || within.equals(aId))) { + id = ObjectId.zeroId(); + src = Text.EMPTY; + srcContent = Text.NO_BYTES; + mode = FileMode.MISSING; + displayMethod = DisplayMethod.NONE; + } else { + id = within; + src = Text.forCommit(reader, within); + srcContent = src.getContent(); + if (src == Text.EMPTY) { + mode = FileMode.MISSING; + displayMethod = DisplayMethod.NONE; + } else { + mode = FileMode.REGULAR_FILE; + } + } + reuse = false; + } else if (Patch.MERGE_LIST.equals(path)) { + if (comparisonType.isAgainstParentOrAutoMerge() + && (aId == within || within.equals(aId))) { + id = ObjectId.zeroId(); + src = Text.EMPTY; + srcContent = Text.NO_BYTES; + mode = FileMode.MISSING; + displayMethod = DisplayMethod.NONE; + } else { + id = within; + src = Text.forMergeList(comparisonType, reader, within); + srcContent = src.getContent(); + if (src == Text.EMPTY) { + mode = FileMode.MISSING; + displayMethod = DisplayMethod.NONE; + } else { + mode = FileMode.REGULAR_FILE; + } + } + reuse = false; + } else { + final TreeWalk tw = find(within); + + id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId(); + mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING; + reuse = + other != null + && other.id.equals(id) + && (other.mode == mode || isBothFile(other.mode, mode)); + + if (reuse) { + srcContent = other.srcContent; + + } else if (mode.getObjectType() == Constants.OBJ_BLOB) { + srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB)); + + } else if (mode.getObjectType() == Constants.OBJ_COMMIT) { + String strContent = "Subproject commit " + ObjectId.toString(id); + srcContent = strContent.getBytes(UTF_8); + + } else { + srcContent = Text.NO_BYTES; + } + + if (reuse) { + mimeType = other.mimeType; + displayMethod = other.displayMethod; + src = other.src; + + } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) { + mimeType = registry.getMimeType(path, srcContent); + if ("image".equals(mimeType.getMediaType()) && registry.isSafeInline(mimeType)) { + displayMethod = DisplayMethod.IMG; + } + } + } + + if (mode == FileMode.MISSING) { + displayMethod = DisplayMethod.NONE; + } + + if (!reuse) { + if (srcContent == Text.NO_BYTES) { + src = Text.EMPTY; + } else { + src = new Text(srcContent); + } + } + + dst.setSize(size()); + + if (mode == FileMode.SYMLINK) { + fileMode = PatchScript.FileMode.SYMLINK; + } else if (mode == FileMode.GITLINK) { + fileMode = PatchScript.FileMode.GITLINK; + } + } catch (IOException err) { + throw new IOException("Cannot read " + within.name() + ":" + path, err); + } + } + + private TreeWalk find(ObjectId within) + throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, + IOException { + if (path == null || within == null) { + return null; + } + try (RevWalk rw = new RevWalk(reader)) { + final RevTree tree = rw.parseTree(within); + return TreeWalk.forPath(reader, path, tree); + } + } + } + + private static boolean isBothFile(FileMode a, FileMode b) { + return (a.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE + && (b.getBits() & FileMode.TYPE_FILE) == FileMode.TYPE_FILE; + } +} |