blob: 8e3a5d142d3b88dca8884b715fd83f1aeb3a2a2a [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.change;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.MoreObjects;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
import com.google.gerrit.extensions.common.DiffInfo.IntraLineStatus;
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.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.prettify.common.SparseFileContent;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.git.LargeObjectException;
import com.google.gerrit.server.patch.PatchScriptFactory;
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.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.ReplaceEdit;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.NamedOptionDef;
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;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class GetDiff implements RestReadView<FileResource> {
private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
Maps.immutableEnumMap(
new ImmutableMap.Builder<Patch.ChangeType, ChangeType>()
.put(Patch.ChangeType.ADDED, ChangeType.ADDED)
.put(Patch.ChangeType.MODIFIED, ChangeType.MODIFIED)
.put(Patch.ChangeType.DELETED, ChangeType.DELETED)
.put(Patch.ChangeType.RENAMED, ChangeType.RENAMED)
.put(Patch.ChangeType.COPIED, ChangeType.COPIED)
.put(Patch.ChangeType.REWRITE, ChangeType.REWRITE)
.build());
private final ProjectCache projectCache;
private final PatchScriptFactory.Factory patchScriptFactoryFactory;
private final Revisions revisions;
private final WebLinks webLinks;
@Option(name = "--base", metaVar = "REVISION")
String base;
@Option(name = "--ignore-whitespace")
IgnoreWhitespace ignoreWhitespace = IgnoreWhitespace.NONE;
@Option(name = "--context", handler = ContextOptionHandler.class)
short context = AccountDiffPreference.DEFAULT_CONTEXT;
@Option(name = "--intraline")
boolean intraline;
@Option(name = "--weblinks-only")
boolean webLinksOnly;
@Inject
GetDiff(ProjectCache projectCache,
PatchScriptFactory.Factory patchScriptFactoryFactory,
Revisions revisions,
WebLinks webLinks) {
this.projectCache = projectCache;
this.patchScriptFactoryFactory = patchScriptFactoryFactory;
this.revisions = revisions;
this.webLinks = webLinks;
}
@Override
public Response<DiffInfo> apply(FileResource resource)
throws ResourceConflictException, ResourceNotFoundException,
OrmException, AuthException, InvalidChangeOperationException, IOException {
PatchSet basePatchSet = null;
if (base != null) {
RevisionResource baseResource = revisions.parse(
resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
basePatchSet = baseResource.getPatchSet();
}
AccountDiffPreference prefs = new AccountDiffPreference(new Account.Id(0));
prefs.setIgnoreWhitespace(ignoreWhitespace.whitespace);
prefs.setContext(context);
prefs.setIntralineDifference(intraline);
try {
PatchScriptFactory psf = patchScriptFactoryFactory.create(
resource.getRevision().getControl(),
resource.getPatchKey().getFileName(),
basePatchSet != null ? basePatchSet.getId() : null,
resource.getPatchKey().getParentKey(),
prefs);
psf.setLoadHistory(false);
psf.setLoadComments(context != AccountDiffPreference.WHOLE_FILE_CONTEXT);
PatchScript ps = psf.call();
Content content = new Content(ps);
for (Edit edit : ps.getEdits()) {
if (edit.getType() == Edit.Type.EMPTY) {
continue;
}
content.addCommon(edit.getBeginA());
checkState(content.nextA == edit.getBeginA(),
"nextA = %s; want %s", content.nextA, edit.getBeginA());
checkState(content.nextB == edit.getBeginB(),
"nextB = %s; want %s", content.nextB, edit.getBeginB());
switch (edit.getType()) {
case DELETE:
case INSERT:
case REPLACE:
List<Edit> internalEdit = edit instanceof ReplaceEdit
? ((ReplaceEdit) edit).getInternalEdits()
: null;
content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit);
break;
case EMPTY:
default:
throw new IllegalStateException();
}
}
content.addCommon(ps.getA().size());
ProjectState state =
projectCache.get(resource.getRevision().getChange().getProject());
DiffInfo result = new DiffInfo();
// TODO referring to the parent commit by refs/changes/12/60012/1^1
// will likely not work for inline edits
String revA = basePatchSet != null
? basePatchSet.getRefName()
: resource.getRevision().getPatchSet().getRefName() + "^1";
String revB = resource.getRevision().getEdit().isPresent()
? resource.getRevision().getEdit().get().getRefName()
: resource.getRevision().getPatchSet().getRefName();
FluentIterable<DiffWebLinkInfo> links =
webLinks.getDiffLinks(state.getProject().getName(),
resource.getPatchKey().getParentKey().getParentKey().get(),
basePatchSet != null ? basePatchSet.getId().get() : null,
revA,
MoreObjects.firstNonNull(ps.getOldName(), ps.getNewName()),
resource.getPatchKey().getParentKey().get(),
revB,
ps.getNewName());
result.webLinks = links.isEmpty() ? null : links.toList();
if (!webLinksOnly) {
if (ps.isBinary()) {
result.binary = true;
}
if (ps.getDisplayMethodA() != DisplayMethod.NONE) {
result.metaA = new FileMeta();
result.metaA.name = MoreObjects.firstNonNull(ps.getOldName(),
ps.getNewName());
result.metaA.contentType = FileContentUtil.resolveContentType(
state, result.metaA.name, ps.getFileModeA(), ps.getMimeTypeA());
result.metaA.lines = ps.getA().size();
result.metaA.webLinks =
getFileWebLinks(state.getProject(), revA, result.metaA.name);
}
if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
result.metaB = new FileMeta();
result.metaB.name = ps.getNewName();
result.metaB.contentType = FileContentUtil.resolveContentType(
state, result.metaB.name, ps.getFileModeB(), ps.getMimeTypeB());
result.metaB.lines = ps.getB().size();
result.metaB.webLinks =
getFileWebLinks(state.getProject(), revB, result.metaB.name);
}
if (intraline) {
if (ps.hasIntralineTimeout()) {
result.intralineStatus = IntraLineStatus.TIMEOUT;
} else if (ps.hasIntralineFailure()) {
result.intralineStatus = IntraLineStatus.FAILURE;
} else {
result.intralineStatus = IntraLineStatus.OK;
}
}
result.changeType = CHANGE_TYPE.get(ps.getChangeType());
if (result.changeType == null) {
throw new IllegalStateException(
"unknown change type: " + ps.getChangeType());
}
if (ps.getPatchHeader().size() > 0) {
result.diffHeader = ps.getPatchHeader();
}
result.content = content.lines;
}
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());
} catch (LargeObjectException e) {
throw new ResourceConflictException(e.getMessage());
}
}
private List<WebLinkInfo> getFileWebLinks(Project project, String rev,
String file) {
FluentIterable<WebLinkInfo> links =
webLinks.getFileLinks(project.getName(), rev, file);
return links.isEmpty() ? null : links.toList();
}
public GetDiff setBase(String base) {
this.base = base;
return this;
}
private static class Content {
final List<ContentEntry> lines;
final SparseFileContent fileA;
final SparseFileContent fileB;
final boolean ignoreWS;
int nextA;
int nextB;
Content(PatchScript ps) {
lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2);
fileA = ps.getA();
fileB = ps.getB();
ignoreWS = ps.isIgnoreWhitespace();
}
void addCommon(int end) {
end = Math.min(end, fileA.size());
if (nextA >= end) {
return;
}
while (nextA < end) {
if (!fileA.contains(nextA)) {
int endRegion = Math.min(
end,
nextA == 0 ? fileA.first() : fileA.next(nextA - 1));
int len = endRegion - nextA;
entry().skip = len;
nextA = endRegion;
nextB += len;
continue;
}
ContentEntry e = null;
for (int i = nextA;
i == nextA && i < end;
i = fileA.next(i), nextA++, nextB++) {
if (ignoreWS && fileB.contains(nextB)) {
if (e == null || e.common == null) {
e = entry();
e.a = Lists.newArrayListWithCapacity(end - nextA);
e.b = Lists.newArrayListWithCapacity(end - nextA);
e.common = true;
}
e.a.add(fileA.get(nextA));
e.b.add(fileB.get(nextB));
} else {
if (e == null || e.common != null) {
e = entry();
e.ab = Lists.newArrayListWithCapacity(end - nextA);
}
e.ab.add(fileA.get(nextA));
}
}
}
}
void addDiff(int endA, int endB, List<Edit> internalEdit) {
int lenA = endA - nextA;
int lenB = endB - nextB;
checkState(lenA > 0 || lenB > 0);
ContentEntry e = entry();
if (lenA > 0) {
e.a = Lists.newArrayListWithCapacity(lenA);
for (; nextA < endA; nextA++) {
e.a.add(fileA.get(nextA));
}
}
if (lenB > 0) {
e.b = Lists.newArrayListWithCapacity(lenB);
for (; nextB < endB; nextB++) {
e.b.add(fileB.get(nextB));
}
}
if (internalEdit != null && !internalEdit.isEmpty()) {
e.editA = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
e.editB = Lists.newArrayListWithCapacity(internalEdit.size() * 2);
int lastA = 0;
int lastB = 0;
for (Edit edit : internalEdit) {
if (edit.getBeginA() != edit.getEndA()) {
e.editA.add(ImmutableList.of(
edit.getBeginA() - lastA, edit.getEndA() - edit.getBeginA()));
lastA = edit.getEndA();
}
if (edit.getBeginB() != edit.getEndB()) {
e.editB.add(ImmutableList.of(
edit.getBeginB() - lastB, edit.getEndB() - edit.getBeginB()));
lastB = edit.getEndB();
}
}
}
}
private ContentEntry entry() {
ContentEntry e = new ContentEntry();
lines.add(e);
return e;
}
}
enum IgnoreWhitespace {
NONE(AccountDiffPreference.Whitespace.IGNORE_NONE),
TRAILING(AccountDiffPreference.Whitespace.IGNORE_SPACE_AT_EOL),
CHANGED(AccountDiffPreference.Whitespace.IGNORE_SPACE_CHANGE),
ALL(AccountDiffPreference.Whitespace.IGNORE_ALL_SPACE);
private final AccountDiffPreference.Whitespace whitespace;
private IgnoreWhitespace(AccountDiffPreference.Whitespace whitespace) {
this.whitespace = whitespace;
}
}
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(final Parameters params)
throws CmdLineException {
final String value = params.getParameter(0);
short context;
if ("all".equalsIgnoreCase(value)) {
context = AccountDiffPreference.WHOLE_FILE_CONTEXT;
} else {
try {
context = Short.parseShort(value, 10);
if (context < 0) {
throw new NumberFormatException();
}
} catch (NumberFormatException e) {
throw new CmdLineException(owner,
String.format("\"%s\" is not a valid value for \"%s\"",
value, ((NamedOptionDef) option).name()));
}
}
setter.addValue(context);
return 1;
}
@Override
public final String getDefaultMetaVariable() {
return "ALL|# LINES";
}
}
}