blob: 3de0f279d9fcd22c9ba7dce676a00b1621b097a8 [file] [log] [blame]
// 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";
}
}
}