| // 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 com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.common.data.PatchScript; | |
| import com.google.gerrit.common.data.PatchScript.DisplayMethod; | |
| import com.google.gerrit.entities.FixReplacement; | |
| import com.google.gerrit.entities.Patch; | |
| import com.google.gerrit.entities.Patch.ChangeType; | |
| import com.google.gerrit.entities.Patch.PatchType; | |
| import com.google.gerrit.extensions.client.DiffPreferencesInfo; | |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; | |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; | |
| import com.google.gerrit.server.fixes.FixCalculator; | |
| import com.google.gerrit.server.mime.FileTypeRegistry; | |
| import com.google.gerrit.server.patch.DiffContentCalculator.DiffCalculatorResult; | |
| import com.google.gerrit.server.patch.DiffContentCalculator.TextSource; | |
| import com.google.gerrit.server.patch.filediff.FileDiffOutput; | |
| import com.google.gerrit.server.patch.filediff.TaggedEdit; | |
| import com.google.inject.Inject; | |
| import eu.medsea.mimeutil.MimeType; | |
| import eu.medsea.mimeutil.MimeUtil2; | |
| import java.io.IOException; | |
| import java.util.List; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.util.Set; | |
| import org.eclipse.jgit.diff.Edit; | |
| 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 { | |
| private DiffPreferencesInfo diffPrefs; | |
| private final FileTypeRegistry registry; | |
| private IntraLineDiffCalculator intralineDiffCalculator; | |
| @Inject | |
| PatchScriptBuilder(FileTypeRegistry ftr) { | |
| registry = ftr; | |
| } | |
| void setDiffPrefs(DiffPreferencesInfo dp) { | |
| diffPrefs = dp; | |
| } | |
| void setIntraLineDiffCalculator(IntraLineDiffCalculator calculator) { | |
| intralineDiffCalculator = calculator; | |
| } | |
| /** Convert into {@link PatchScript} using the new diff cache output. */ | |
| PatchScript toPatchScript(Repository git, FileDiffOutput content) throws IOException { | |
| PatchFileChange change = | |
| new PatchFileChange( | |
| content.edits().stream().map(TaggedEdit::jgitEdit).collect(toImmutableList()), | |
| content.edits().stream() | |
| .filter(TaggedEdit::dueToRebase) | |
| .map(TaggedEdit::jgitEdit) | |
| .collect(toImmutableSet()), | |
| content.headerLines(), | |
| FilePathAdapter.getOldPath(content.oldPath(), content.changeType()), | |
| FilePathAdapter.getNewPath(content.oldPath(), content.newPath(), content.changeType()), | |
| content.changeType(), | |
| content.patchType().orElse(null)); | |
| SidesResolver sidesResolver = new SidesResolver(git, content.comparisonType()); | |
| ResolvedSides sides = | |
| resolveSides( | |
| git, | |
| sidesResolver, | |
| oldName(change), | |
| newName(change), | |
| content.oldCommitId(), | |
| content.newCommitId()); | |
| return build(sides.a, sides.b, change); | |
| } | |
| private ResolvedSides resolveSides( | |
| Repository git, | |
| SidesResolver sidesResolver, | |
| String oldName, | |
| String newName, | |
| ObjectId aId, | |
| ObjectId bId) | |
| throws IOException { | |
| try (ObjectReader reader = git.newObjectReader()) { | |
| PatchSide a = sidesResolver.resolve(registry, reader, oldName, null, aId, true); | |
| PatchSide b = | |
| sidesResolver.resolve(registry, reader, newName, a, bId, Objects.equals(aId, bId)); | |
| return new ResolvedSides(a, b); | |
| } | |
| } | |
| PatchScript toPatchScript( | |
| Repository git, ObjectId baseId, String fileName, List<FixReplacement> fixReplacements) | |
| throws IOException, ResourceConflictException, ResourceNotFoundException { | |
| SidesResolver sidesResolver = new SidesResolver(git, ComparisonType.againstOtherPatchSet()); | |
| PatchSide a = resolveSideA(git, sidesResolver, fileName, baseId); | |
| if (a.mode == FileMode.MISSING) { | |
| throw new ResourceNotFoundException(String.format("File %s not found", fileName)); | |
| } | |
| FixCalculator.FixResult fixResult = FixCalculator.calculateFix(a.src, fixReplacements, true); | |
| PatchSide b = | |
| new PatchSide( | |
| null, | |
| fileName, | |
| ObjectId.zeroId(), | |
| a.mode, | |
| fixResult.text.getContent(), | |
| fixResult.text, | |
| a.mimeType, | |
| a.displayMethod, | |
| a.fileMode); | |
| PatchFileChange change = | |
| new PatchFileChange( | |
| fixResult.edits, | |
| ImmutableSet.of(), | |
| ImmutableList.of(), | |
| fileName, | |
| fileName, | |
| ChangeType.MODIFIED, | |
| PatchType.UNIFIED); | |
| return build(a, b, change); | |
| } | |
| private PatchSide resolveSideA( | |
| Repository git, SidesResolver sidesResolver, String path, ObjectId baseId) | |
| throws IOException { | |
| try (ObjectReader reader = git.newObjectReader()) { | |
| return sidesResolver.resolve(registry, reader, path, null, baseId, true); | |
| } | |
| } | |
| private PatchScript build(PatchSide a, PatchSide b, PatchFileChange content) { | |
| ImmutableList<Edit> contentEdits = content.getEdits(); | |
| ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase(); | |
| IntraLineDiffCalculatorResult intralineResult = IntraLineDiffCalculatorResult.NO_RESULT; | |
| if (isModify(content) && intralineDiffCalculator != null && isIntralineModeAllowed(b)) { | |
| intralineResult = | |
| intralineDiffCalculator.calculateIntraLineDiff( | |
| contentEdits, editsDueToRebase, a.id, b.id, a.src, b.src, b.treeId, b.path); | |
| } | |
| ImmutableList<Edit> finalEdits = intralineResult.edits.orElse(contentEdits); | |
| DiffContentCalculator calculator = new DiffContentCalculator(diffPrefs); | |
| DiffCalculatorResult diffCalculatorResult = | |
| calculator.calculateDiffContent(new TextSource(a.src), new TextSource(b.src), finalEdits); | |
| return new PatchScript( | |
| content.getChangeType(), | |
| content.getOldName(), | |
| content.getNewName(), | |
| a.fileMode, | |
| b.fileMode, | |
| content.getHeaderLines(), | |
| diffPrefs, | |
| diffCalculatorResult.diffContent.a, | |
| diffCalculatorResult.diffContent.b, | |
| diffCalculatorResult.edits, | |
| editsDueToRebase, | |
| a.displayMethod, | |
| b.displayMethod, | |
| a.mimeType, | |
| b.mimeType, | |
| intralineResult.failure, | |
| intralineResult.timeout, | |
| content.getPatchType() == Patch.PatchType.BINARY, | |
| a.treeId == null ? null : a.treeId.getName(), | |
| b.treeId == null ? null : b.treeId.getName()); | |
| } | |
| private static boolean isModify(PatchFileChange content) { | |
| switch (content.getChangeType()) { | |
| case MODIFIED: | |
| case COPIED: | |
| case RENAMED: | |
| case REWRITE: | |
| return true; | |
| case ADDED: | |
| case DELETED: | |
| default: | |
| return false; | |
| } | |
| } | |
| @Nullable | |
| private static String oldName(PatchFileChange entry) { | |
| switch (entry.getChangeType()) { | |
| case ADDED: | |
| return null; | |
| case DELETED: | |
| case MODIFIED: | |
| return entry.getNewName(); | |
| case COPIED: | |
| case RENAMED: | |
| case REWRITE: | |
| default: | |
| return entry.getOldName(); | |
| } | |
| } | |
| @Nullable | |
| private static String newName(PatchFileChange entry) { | |
| switch (entry.getChangeType()) { | |
| case DELETED: | |
| return null; | |
| case ADDED: | |
| case MODIFIED: | |
| case COPIED: | |
| case RENAMED: | |
| case REWRITE: | |
| default: | |
| return entry.getNewName(); | |
| } | |
| } | |
| private static boolean isIntralineModeAllowed(PatchSide side) { | |
| // The intraline diff cache keys are the same for these cases. It's better to not show | |
| // intraline results than showing completely wrong diffs or to run into a server error. | |
| return !Patch.isMagic(side.path) && !isSubmoduleCommit(side.mode); | |
| } | |
| private static boolean isSubmoduleCommit(FileMode mode) { | |
| return mode.getObjectType() == Constants.OBJ_COMMIT; | |
| } | |
| private static class PatchSide { | |
| final ObjectId treeId; | |
| final String path; | |
| final ObjectId id; | |
| final FileMode mode; | |
| final byte[] srcContent; | |
| final Text src; | |
| final String mimeType; | |
| final DisplayMethod displayMethod; | |
| final PatchScript.FileMode fileMode; | |
| private PatchSide( | |
| ObjectId treeId, | |
| String path, | |
| ObjectId id, | |
| FileMode mode, | |
| byte[] srcContent, | |
| Text src, | |
| String mimeType, | |
| DisplayMethod displayMethod, | |
| PatchScript.FileMode fileMode) { | |
| this.treeId = treeId; | |
| this.path = path; | |
| this.id = id; | |
| this.mode = mode; | |
| this.srcContent = srcContent; | |
| this.src = src; | |
| this.mimeType = mimeType; | |
| this.displayMethod = displayMethod; | |
| this.fileMode = fileMode; | |
| } | |
| } | |
| private static class ResolvedSides { | |
| // Not an @AutoValue because PatchSide can't be AutoValue | |
| public final PatchSide a; | |
| public final PatchSide b; | |
| ResolvedSides(PatchSide a, PatchSide b) { | |
| this.a = a; | |
| this.b = b; | |
| } | |
| } | |
| static class SidesResolver { | |
| private final Repository db; | |
| private final ComparisonType comparisonType; | |
| SidesResolver(Repository db, ComparisonType comparisonType) { | |
| this.db = db; | |
| this.comparisonType = comparisonType; | |
| } | |
| PatchSide resolve( | |
| final FileTypeRegistry registry, | |
| final ObjectReader reader, | |
| final String path, | |
| final PatchSide other, | |
| final ObjectId within, | |
| final boolean isWithinEqualsA) | |
| throws IOException { | |
| try { | |
| boolean isCommitMsg = Patch.COMMIT_MSG.equals(path); | |
| boolean isMergeList = Patch.MERGE_LIST.equals(path); | |
| if (isCommitMsg || isMergeList) { | |
| if (comparisonType.isAgainstParentOrAutoMerge() && isWithinEqualsA) { | |
| return createSide( | |
| within, | |
| path, | |
| ObjectId.zeroId(), | |
| FileMode.MISSING, | |
| Text.NO_BYTES, | |
| Text.EMPTY, | |
| MimeUtil2.UNKNOWN_MIME_TYPE.toString(), | |
| DisplayMethod.NONE, | |
| false); | |
| } | |
| Text src = | |
| isCommitMsg | |
| ? Text.forCommit(reader, within) | |
| : Text.forMergeList(comparisonType, reader, within); | |
| byte[] srcContent = src.getContent(); | |
| DisplayMethod displayMethod; | |
| FileMode mode; | |
| if (src == Text.EMPTY) { | |
| mode = FileMode.MISSING; | |
| displayMethod = DisplayMethod.NONE; | |
| } else { | |
| mode = FileMode.REGULAR_FILE; | |
| displayMethod = DisplayMethod.DIFF; | |
| } | |
| return createSide( | |
| within, | |
| path, | |
| within, | |
| mode, | |
| srcContent, | |
| src, | |
| MimeUtil2.UNKNOWN_MIME_TYPE.toString(), | |
| displayMethod, | |
| false); | |
| } | |
| final TreeWalk tw = find(reader, path, within); | |
| ObjectId id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId(); | |
| FileMode mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING; | |
| boolean reuse = | |
| other != null | |
| && other.id.equals(id) | |
| && (other.mode == mode || isBothFile(other.mode, mode)); | |
| Text src = null; | |
| byte[] srcContent; | |
| if (reuse) { | |
| srcContent = other.srcContent; | |
| } else { | |
| srcContent = SrcContentResolver.getSourceContent(db, id, mode); | |
| } | |
| String mimeType = MimeUtil2.UNKNOWN_MIME_TYPE.toString(); | |
| DisplayMethod displayMethod = DisplayMethod.DIFF; | |
| if (reuse) { | |
| mimeType = other.mimeType; | |
| displayMethod = other.displayMethod; | |
| src = other.src; | |
| } else if (srcContent.length > 0 && FileMode.SYMLINK != mode) { | |
| MimeType registryMimeType = registry.getMimeType(path, srcContent); | |
| if ("image".equals(registryMimeType.getMediaType()) | |
| && registry.isSafeInline(registryMimeType)) { | |
| displayMethod = DisplayMethod.IMG; | |
| } | |
| mimeType = registryMimeType.toString(); | |
| } | |
| return createSide(within, path, id, mode, srcContent, src, mimeType, displayMethod, reuse); | |
| } catch (IOException err) { | |
| throw new IOException("Cannot read " + within.name() + ":" + path, err); | |
| } | |
| } | |
| private PatchSide createSide( | |
| ObjectId treeId, | |
| String path, | |
| ObjectId id, | |
| FileMode mode, | |
| byte[] srcContent, | |
| Text src, | |
| String mimeType, | |
| DisplayMethod displayMethod, | |
| boolean reuse) { | |
| if (!reuse) { | |
| if (srcContent == Text.NO_BYTES) { | |
| src = Text.EMPTY; | |
| } else { | |
| src = new Text(srcContent); | |
| } | |
| } | |
| if (mode == FileMode.MISSING) { | |
| displayMethod = DisplayMethod.NONE; | |
| } | |
| PatchScript.FileMode fileMode = PatchScript.FileMode.fromJgitFileMode(mode); | |
| return new PatchSide( | |
| treeId, path, id, mode, srcContent, src, mimeType, displayMethod, fileMode); | |
| } | |
| @Nullable | |
| private TreeWalk find(ObjectReader reader, String path, ObjectId within) throws 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; | |
| } | |
| static class IntraLineDiffCalculatorResult { | |
| // Not an @AutoValue because Edit is mutable | |
| final boolean failure; | |
| final boolean timeout; | |
| private final Optional<ImmutableList<Edit>> edits; | |
| private IntraLineDiffCalculatorResult( | |
| Optional<ImmutableList<Edit>> edits, boolean failure, boolean timeout) { | |
| this.failure = failure; | |
| this.timeout = timeout; | |
| this.edits = edits; | |
| } | |
| static final IntraLineDiffCalculatorResult NO_RESULT = | |
| new IntraLineDiffCalculatorResult(Optional.empty(), false, false); | |
| static final IntraLineDiffCalculatorResult FAILURE = | |
| new IntraLineDiffCalculatorResult(Optional.empty(), true, false); | |
| static final IntraLineDiffCalculatorResult TIMEOUT = | |
| new IntraLineDiffCalculatorResult(Optional.empty(), false, true); | |
| static IntraLineDiffCalculatorResult success(ImmutableList<Edit> edits) { | |
| return new IntraLineDiffCalculatorResult(Optional.of(edits), false, false); | |
| } | |
| } | |
| interface IntraLineDiffCalculator { | |
| IntraLineDiffCalculatorResult calculateIntraLineDiff( | |
| ImmutableList<Edit> edits, | |
| Set<Edit> editsDueToRebase, | |
| ObjectId aId, | |
| ObjectId bId, | |
| Text aSrc, | |
| Text bSrc, | |
| ObjectId bTreeId, | |
| String bPath); | |
| } | |
| static class PatchFileChange { | |
| private final ImmutableList<Edit> edits; | |
| private final ImmutableSet<Edit> editsDueToRebase; | |
| private final ImmutableList<String> headerLines; | |
| private final String oldName; | |
| private final String newName; | |
| private final ChangeType changeType; | |
| private final Patch.PatchType patchType; | |
| public PatchFileChange( | |
| ImmutableList<Edit> edits, | |
| ImmutableSet<Edit> editsDueToRebase, | |
| ImmutableList<String> headerLines, | |
| String oldName, | |
| String newName, | |
| ChangeType changeType, | |
| Patch.PatchType patchType) { | |
| this.edits = edits; | |
| this.editsDueToRebase = editsDueToRebase; | |
| this.headerLines = headerLines; | |
| this.oldName = oldName; | |
| this.newName = newName; | |
| this.changeType = changeType; | |
| this.patchType = patchType; | |
| } | |
| ImmutableList<Edit> getEdits() { | |
| return edits; | |
| } | |
| ImmutableSet<Edit> getEditsDueToRebase() { | |
| return editsDueToRebase; | |
| } | |
| ImmutableList<String> getHeaderLines() { | |
| return headerLines; | |
| } | |
| String getNewName() { | |
| return newName; | |
| } | |
| String getOldName() { | |
| return oldName; | |
| } | |
| ChangeType getChangeType() { | |
| return changeType; | |
| } | |
| Patch.PatchType getPatchType() { | |
| return patchType; | |
| } | |
| } | |
| } |