|  | // Copyright (C) 2013 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.restapi.change; | 
|  |  | 
|  | import static com.google.gerrit.server.project.ProjectCache.illegalState; | 
|  |  | 
|  | import com.google.common.base.MoreObjects; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.flogger.FluentLogger; | 
|  | import com.google.gerrit.common.Nullable; | 
|  | import com.google.gerrit.common.data.PatchScript; | 
|  | import com.google.gerrit.entities.PatchSet; | 
|  | import com.google.gerrit.entities.Project; | 
|  | import com.google.gerrit.extensions.client.DiffPreferencesInfo; | 
|  | import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; | 
|  | import com.google.gerrit.extensions.common.DiffInfo; | 
|  | import com.google.gerrit.extensions.common.DiffWebLinkInfo; | 
|  | import com.google.gerrit.extensions.common.WebLinkInfo; | 
|  | import com.google.gerrit.extensions.restapi.AuthException; | 
|  | import com.google.gerrit.extensions.restapi.BadRequestException; | 
|  | import com.google.gerrit.extensions.restapi.CacheControl; | 
|  | import com.google.gerrit.extensions.restapi.IdString; | 
|  | import com.google.gerrit.extensions.restapi.ResourceConflictException; | 
|  | import com.google.gerrit.extensions.restapi.ResourceNotFoundException; | 
|  | import com.google.gerrit.extensions.restapi.Response; | 
|  | import com.google.gerrit.extensions.restapi.RestReadView; | 
|  | import com.google.gerrit.server.CurrentUser; | 
|  | import com.google.gerrit.server.WebLinks; | 
|  | import com.google.gerrit.server.change.FileResource; | 
|  | import com.google.gerrit.server.change.RevisionResource; | 
|  | import com.google.gerrit.server.diff.DiffInfoCreator; | 
|  | import com.google.gerrit.server.diff.DiffSide; | 
|  | import com.google.gerrit.server.diff.DiffWebLinksProvider; | 
|  | import com.google.gerrit.server.git.LargeObjectException; | 
|  | import com.google.gerrit.server.notedb.ChangeNotes; | 
|  | import com.google.gerrit.server.patch.PatchScriptFactory; | 
|  | import com.google.gerrit.server.permissions.PermissionBackendException; | 
|  | import com.google.gerrit.server.project.InvalidChangeOperationException; | 
|  | import com.google.gerrit.server.project.NoSuchChangeException; | 
|  | import com.google.gerrit.server.project.ProjectCache; | 
|  | import com.google.gerrit.server.project.ProjectState; | 
|  | import com.google.inject.Inject; | 
|  | import com.google.inject.Provider; | 
|  | import java.io.IOException; | 
|  | import java.util.concurrent.TimeUnit; | 
|  | import org.kohsuke.args4j.CmdLineParser; | 
|  | import org.kohsuke.args4j.Option; | 
|  | import org.kohsuke.args4j.OptionDef; | 
|  | import org.kohsuke.args4j.spi.OptionHandler; | 
|  | import org.kohsuke.args4j.spi.Parameters; | 
|  | import org.kohsuke.args4j.spi.Setter; | 
|  |  | 
|  | public class GetDiff implements RestReadView<FileResource> { | 
|  | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | 
|  |  | 
|  | private final ProjectCache projectCache; | 
|  | private final PatchScriptFactory.Factory patchScriptFactoryFactory; | 
|  | private final Revisions revisions; | 
|  | private final WebLinks webLinks; | 
|  | private final Provider<CurrentUser> currentUser; | 
|  |  | 
|  | @Option(name = "--base", metaVar = "REVISION") | 
|  | String base; | 
|  |  | 
|  | /** 1-based index of the parent's position in the commit object. */ | 
|  | @Option(name = "--parent", metaVar = "parent-number") | 
|  | int parentNum; | 
|  |  | 
|  | @Deprecated | 
|  | @Option(name = "--ignore-whitespace") | 
|  | IgnoreWhitespace ignoreWhitespace; | 
|  |  | 
|  | @Option(name = "--whitespace") | 
|  | Whitespace whitespace; | 
|  |  | 
|  | // TODO(hiesel): Remove parameter when not used by callers (e.g. frontend) anymore. | 
|  | @Option(name = "--context", handler = ContextOptionHandler.class) | 
|  | int context; | 
|  |  | 
|  | @Option(name = "--intraline") | 
|  | boolean intraline; | 
|  |  | 
|  | @Inject | 
|  | GetDiff( | 
|  | ProjectCache projectCache, | 
|  | PatchScriptFactory.Factory patchScriptFactoryFactory, | 
|  | Revisions revisions, | 
|  | WebLinks webLinks, | 
|  | Provider<CurrentUser> currentUser) { | 
|  | this.projectCache = projectCache; | 
|  | this.patchScriptFactoryFactory = patchScriptFactoryFactory; | 
|  | this.revisions = revisions; | 
|  | this.webLinks = webLinks; | 
|  | this.currentUser = currentUser; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public Response<DiffInfo> apply(FileResource resource) | 
|  | throws BadRequestException, ResourceConflictException, ResourceNotFoundException, | 
|  | AuthException, InvalidChangeOperationException, IOException, PermissionBackendException { | 
|  | DiffPreferencesInfo prefs = new DiffPreferencesInfo(); | 
|  | if (whitespace != null) { | 
|  | prefs.ignoreWhitespace = whitespace; | 
|  | } else if (ignoreWhitespace != null) { | 
|  | prefs.ignoreWhitespace = ignoreWhitespace.whitespace; | 
|  | } else { | 
|  | prefs.ignoreWhitespace = Whitespace.IGNORE_LEADING_AND_TRAILING; | 
|  | } | 
|  | prefs.intralineDifference = intraline; | 
|  | logger.atFine().log( | 
|  | "diff preferences: ignoreWhitespace = %s, intralineDifference = %s", | 
|  | prefs.ignoreWhitespace, prefs.intralineDifference); | 
|  |  | 
|  | PatchScriptFactory psf; | 
|  | PatchSet basePatchSet = null; | 
|  | PatchSet.Id pId = resource.getPatchKey().patchSetId(); | 
|  | String fileName = resource.getPatchKey().fileName(); | 
|  | logger.atFine().log( | 
|  | "patchSetId = %d, fileName = %s, base = %s, parentNum = %d", | 
|  | pId.get(), fileName, base, parentNum); | 
|  | ChangeNotes notes = resource.getRevision().getNotes(); | 
|  | if (base != null) { | 
|  | RevisionResource baseResource = | 
|  | revisions.parse(resource.getRevision().getChangeResource(), IdString.fromDecoded(base)); | 
|  | basePatchSet = baseResource.getPatchSet(); | 
|  | if (basePatchSet.id().get() == 0) { | 
|  | throw new BadRequestException("edit not allowed as base"); | 
|  | } | 
|  | psf = | 
|  | patchScriptFactoryFactory.create( | 
|  | notes, fileName, basePatchSet.id(), pId, prefs, currentUser.get()); | 
|  | } else if (parentNum > 0) { | 
|  | psf = | 
|  | patchScriptFactoryFactory.create( | 
|  | notes, fileName, parentNum, pId, prefs, currentUser.get()); | 
|  | } else { | 
|  | psf = patchScriptFactoryFactory.create(notes, fileName, null, pId, prefs, currentUser.get()); | 
|  | } | 
|  |  | 
|  | try { | 
|  | PatchScript ps = psf.call(); | 
|  | Project.NameKey projectName = resource.getRevision().getChange().getProject(); | 
|  | ProjectState state = projectCache.get(projectName).orElseThrow(illegalState(projectName)); | 
|  | DiffSide sideA = | 
|  | DiffSide.create( | 
|  | ps.getFileInfoA(), | 
|  | MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()), | 
|  | DiffSide.Type.SIDE_A); | 
|  | DiffSide sideB = DiffSide.create(ps.getFileInfoB(), ps.getNewName(), DiffSide.Type.SIDE_B); | 
|  | DiffWebLinksProvider webLinksProvider = | 
|  | new DiffWebLinksProviderImpl(sideA, sideB, projectName, basePatchSet, webLinks, resource); | 
|  | DiffInfoCreator diffInfoCreator = new DiffInfoCreator(state, webLinksProvider, intraline); | 
|  | DiffInfo result = diffInfoCreator.create(ps, sideA, sideB); | 
|  |  | 
|  | Response<DiffInfo> r = Response.ok(result); | 
|  | if (resource.isCacheable()) { | 
|  | r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS)); | 
|  | } | 
|  | return r; | 
|  | } catch (NoSuchChangeException e) { | 
|  | throw new ResourceNotFoundException(e.getMessage(), e); | 
|  | } catch (LargeObjectException e) { | 
|  | throw new ResourceConflictException(e.getMessage(), e); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static class DiffWebLinksProviderImpl implements DiffWebLinksProvider { | 
|  |  | 
|  | private final WebLinks webLinks; | 
|  | private final Project.NameKey projectName; | 
|  | private final DiffSide sideA; | 
|  | private final DiffSide sideB; | 
|  | private final String revA; | 
|  | private final String revB; | 
|  | private final String hashA; | 
|  | private final String hashB; | 
|  | private final FileResource resource; | 
|  | @Nullable private final PatchSet basePatchSet; | 
|  |  | 
|  | DiffWebLinksProviderImpl( | 
|  | DiffSide sideA, | 
|  | DiffSide sideB, | 
|  | Project.NameKey projectName, | 
|  | @Nullable PatchSet basePatchSet, | 
|  | WebLinks webLinks, | 
|  | FileResource resource) { | 
|  | this.projectName = projectName; | 
|  | this.webLinks = webLinks; | 
|  | this.basePatchSet = basePatchSet; | 
|  | this.resource = resource; | 
|  | this.sideA = sideA; | 
|  | this.sideB = sideB; | 
|  |  | 
|  | revA = basePatchSet != null ? basePatchSet.refName() : sideA.fileInfo().commitId; | 
|  | hashA = sideA.fileInfo().commitId; | 
|  |  | 
|  | RevisionResource revision = resource.getRevision(); | 
|  | revB = | 
|  | revision | 
|  | .getEdit() | 
|  | .map(edit -> edit.getRefName()) | 
|  | .orElseGet(() -> revision.getPatchSet().refName()); | 
|  | hashB = sideB.fileInfo().commitId; | 
|  |  | 
|  | logger.atFine().log("revA = %s, hashA = %s, revB = %s, hashB = %s", revA, hashA, revB, hashB); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public ImmutableList<DiffWebLinkInfo> getDiffLinks() { | 
|  | return webLinks.getDiffLinks( | 
|  | projectName.get(), | 
|  | resource.getPatchKey().patchSetId().changeId().get(), | 
|  | basePatchSet != null ? basePatchSet.id().get() : null, | 
|  | revA, | 
|  | sideA.fileName(), | 
|  | resource.getPatchKey().patchSetId().get(), | 
|  | revB, | 
|  | sideB.fileName()); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public ImmutableList<WebLinkInfo> getEditWebLinks() { | 
|  | return webLinks.getEditLinks(projectName.get(), revB, sideB.fileName()); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public ImmutableList<WebLinkInfo> getFileWebLinks(DiffSide.Type type) { | 
|  | String rev = getSideRev(type); | 
|  | String hash = getSideHash(type); | 
|  | DiffSide side = getDiffSide(type); | 
|  | return webLinks.getFileLinks(projectName.get(), rev, hash, side.fileName()); | 
|  | } | 
|  |  | 
|  | private String getSideRev(DiffSide.Type sideType) { | 
|  | return DiffSide.Type.SIDE_A == sideType ? revA : revB; | 
|  | } | 
|  |  | 
|  | private String getSideHash(DiffSide.Type sideType) { | 
|  | return DiffSide.Type.SIDE_A == sideType ? hashA : hashB; | 
|  | } | 
|  |  | 
|  | private DiffSide getDiffSide(DiffSide.Type sideType) { | 
|  | return DiffSide.Type.SIDE_A == sideType ? sideA : sideB; | 
|  | } | 
|  | } | 
|  |  | 
|  | public GetDiff setBase(String base) { | 
|  | this.base = base; | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public GetDiff setParent(int parentNum) { | 
|  | this.parentNum = parentNum; | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public GetDiff setIntraline(boolean intraline) { | 
|  | this.intraline = intraline; | 
|  | return this; | 
|  | } | 
|  |  | 
|  | public GetDiff setWhitespace(Whitespace whitespace) { | 
|  | this.whitespace = whitespace; | 
|  | return this; | 
|  | } | 
|  |  | 
|  | @Deprecated | 
|  | enum IgnoreWhitespace { | 
|  | NONE(DiffPreferencesInfo.Whitespace.IGNORE_NONE), | 
|  | TRAILING(DiffPreferencesInfo.Whitespace.IGNORE_TRAILING), | 
|  | CHANGED(DiffPreferencesInfo.Whitespace.IGNORE_LEADING_AND_TRAILING), | 
|  | ALL(DiffPreferencesInfo.Whitespace.IGNORE_ALL); | 
|  |  | 
|  | private final DiffPreferencesInfo.Whitespace whitespace; | 
|  |  | 
|  | IgnoreWhitespace(DiffPreferencesInfo.Whitespace whitespace) { | 
|  | this.whitespace = whitespace; | 
|  | } | 
|  | } | 
|  |  | 
|  | // TODO(hiesel): Remove this class once clients don't send the context parameter anymore. | 
|  | public static class ContextOptionHandler extends OptionHandler<Short> { | 
|  |  | 
|  | public ContextOptionHandler(CmdLineParser parser, OptionDef option, Setter<Short> setter) { | 
|  | super(parser, option, setter); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public final int parseArguments(Parameters params) { | 
|  | // Return 1 to consume the context parameter. | 
|  | return 1; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public final String getDefaultMetaVariable() { | 
|  | return "ignored"; | 
|  | } | 
|  | } | 
|  | } |