| // 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.errorprone.annotations.CanIgnoreReturnValue; |
| 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; |
| } |
| } |
| |
| @CanIgnoreReturnValue |
| public GetDiff setBase(String base) { |
| this.base = base; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public GetDiff setParent(int parentNum) { |
| this.parentNum = parentNum; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public GetDiff setIntraline(boolean intraline) { |
| this.intraline = intraline; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| 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"; |
| } |
| } |
| } |