diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java index fecbb7656c..914e69f0bd 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java @@ -48,6 +48,8 @@ public class PatchScript { protected List edits; protected DisplayMethod displayMethodA; protected DisplayMethod displayMethodB; + protected transient String mimeTypeA; + protected transient String mimeTypeB; protected CommentDetail comments; protected List history; protected boolean hugeFile; @@ -60,8 +62,9 @@ public class PatchScript { final List h, final AccountDiffPreference dp, final SparseFileContent ca, final SparseFileContent cb, final List e, final DisplayMethod ma, final DisplayMethod mb, - final CommentDetail cd, final List hist, final boolean hf, - final boolean id, final boolean idf, final boolean idt) { + final String mta, final String mtb, final CommentDetail cd, + final List hist, final boolean hf, final boolean id, + final boolean idf, final boolean idt) { changeId = ck; changeType = ct; oldName = on; @@ -75,6 +78,8 @@ public class PatchScript { edits = e; displayMethodA = ma; displayMethodB = mb; + mimeTypeA = mta; + mimeTypeB = mtb; comments = cd; history = hist; hugeFile = hf; @@ -170,6 +175,14 @@ public class PatchScript { return b; } + public String getMimeTypeA() { + return mimeTypeA; + } + + public String getMimeTypeB() { + return mimeTypeB; + } + public List getEdits() { return edits; } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java index 756cb161c3..12289aa35f 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java @@ -32,6 +32,7 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.changedetail.DeleteDraftPatchSet; import com.google.gerrit.server.patch.PatchScriptFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gwtjsonrpc.common.AsyncCallback; @@ -51,6 +52,7 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements private final PatchScriptFactory.Factory patchScriptFactoryFactory; private final SaveDraft.Factory saveDraftFactory; private final ChangeDetailFactory.Factory changeDetailFactory; + private final ChangeControl.Factory changeControlFactory; @Inject PatchDetailServiceImpl(final Provider schema, @@ -58,13 +60,15 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements final DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory, final PatchScriptFactory.Factory patchScriptFactoryFactory, final SaveDraft.Factory saveDraftFactory, - final ChangeDetailFactory.Factory changeDetailFactory) { + final ChangeDetailFactory.Factory changeDetailFactory, + final ChangeControl.Factory changeControlFactory) { super(schema, currentUser); this.deleteDraftPatchSetFactory = deleteDraftPatchSetFactory; this.patchScriptFactoryFactory = patchScriptFactoryFactory; this.saveDraftFactory = saveDraftFactory; this.changeDetailFactory = changeDetailFactory; + this.changeControlFactory = changeControlFactory; } public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa, @@ -74,8 +78,16 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements callback.onFailure(new NoSuchEntityException()); return; } - Handler.wrap(patchScriptFactoryFactory.create(patchKey, psa, psb, dp)) - .to(callback); + + new Handler() { + @Override + public PatchScript call() throws Exception { + Change.Id changeId = patchKey.getParentKey().getParentKey(); + ChangeControl control = changeControlFactory.validateFor(changeId); + return patchScriptFactoryFactory.create( + control, patchKey.getFileName(), psa, psb, dp).call(); + } + }.to(callback); } public void saveDraft(final PatchLineComment comment, diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java new file mode 100644 index 0000000000..ac2d8badbb --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java @@ -0,0 +1,335 @@ +// 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.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.gerrit.common.data.PatchScript; +import com.google.gerrit.common.data.PatchScript.DisplayMethod; +import com.google.gerrit.common.data.PatchScript.FileMode; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +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.ChangeType; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.server.patch.PatchScriptFactory; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.git.LargeObjectException; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +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.util.List; + +public class GetDiff implements RestReadView { + private final PatchScriptFactory.Factory patchScriptFactoryFactory; + private final Provider revisions; + + @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; + + @Inject + GetDiff(PatchScriptFactory.Factory patchScriptFactoryFactory, + Provider revisions) { + this.patchScriptFactoryFactory = patchScriptFactoryFactory; + this.revisions = revisions; + } + + @Override + public Object apply(FileResource resource) + throws OrmException, NoSuchChangeException, LargeObjectException, ResourceNotFoundException { + PatchSet.Id basePatchSet = null; + if (base != null) { + RevisionResource baseResource = revisions.get().parse( + resource.getRevision().getChangeResource(), IdString.fromDecoded(base)); + basePatchSet = baseResource.getPatchSet().getId(); + } + AccountDiffPreference prefs = new AccountDiffPreference(new Account.Id(0)); + prefs.setIgnoreWhitespace(ignoreWhitespace.whitespace); + prefs.setContext(context); + prefs.setIntralineDifference(intraline); + + PatchScript ps = patchScriptFactoryFactory.create( + resource.getRevision().getControl(), + resource.getPatchKey().getFileName(), + basePatchSet, + resource.getPatchKey().getParentKey(), + prefs) + .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 = %d; want %d", content.nextA, edit.getBeginA()); + checkState(content.nextB == edit.getBeginB(), + "nextB = %d; want %d", content.nextB, edit.getBeginB()); + switch (edit.getType()) { + case DELETE: + case INSERT: + case REPLACE: + List 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()); + + Result result = new Result(); + if (ps.getDisplayMethodA() != DisplayMethod.NONE) { + result.metaA = new FileMeta(); + result.metaA.name = Objects.firstNonNull(ps.getOldName(), ps.getNewName()); + result.metaA.setContentType(ps.getFileModeA(), ps.getMimeTypeA()); + } + + if (ps.getDisplayMethodB() != DisplayMethod.NONE) { + result.metaB = new FileMeta(); + result.metaB.name = ps.getNewName(); + result.metaB.setContentType(ps.getFileModeB(), ps.getMimeTypeB()); + } + + if (intraline) { + if (ps.hasIntralineTimeout()) { + result.intralineStatus = IntraLineStatus.TIMEOUT; + } else if (ps.hasIntralineFailure()) { + result.intralineStatus = IntraLineStatus.FAILURE; + } else { + result.intralineStatus = IntraLineStatus.OK; + } + } + + result.changeType = ps.getChangeType(); + if (ps.getPatchHeader().size() > 0) { + result.diffHeader = ps.getPatchHeader(); + } + result.content = content.lines; + return result; + } + + static class Result { + FileMeta metaA; + FileMeta metaB; + IntraLineStatus intralineStatus; + ChangeType changeType; + List diffHeader; + List content; + } + + static class FileMeta { + String name; + String contentType; + String url; + + void setContentType(FileMode fileMode, String mimeType) { + switch (fileMode) { + case FILE: + contentType = mimeType; + break; + case GITLINK: + contentType = "x-git/gitlink"; + break; + case SYMLINK: + contentType = "x-git/symlink"; + break; + default: + throw new IllegalStateException("file mode: " + fileMode); + } + } + } + + enum IntraLineStatus { + OK, + TIMEOUT, + FAILURE; + } + + private static class Content { + final List lines; + final SparseFileContent fileA; + final SparseFileContent fileB; + + int nextA; + int nextB; + + Content(PatchScript ps) { + lines = Lists.newArrayListWithExpectedSize(ps.getEdits().size() + 2); + fileA = ps.getA(); + fileB = ps.getB(); + } + + void addCommon(int end) { + end = Math.min(end, fileA.size()); + if (nextA >= end) { + return; + } + nextB += end - nextA; + + while (nextA < end) { + if (fileA.contains(nextA)) { + ContentEntry e = entry(); + e.ab = Lists.newArrayListWithCapacity(end - nextA); + for (int i = nextA; i == nextA && i < end; i = fileA.next(i), nextA++) { + e.ab.add(fileA.get(i)); + } + } else { + int endRegion = Math.min(end, + (nextA == 0) ? fileA.first() : fileA.next(nextA - 1)); + ContentEntry e = entry(); + e.skip = endRegion - nextA; + nextA = endRegion; + } + } + } + + void addDiff(int endA, int endB, List 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); + for (Edit edit : internalEdit) { + if (edit.getBeginA() != edit.getEndA()) { + e.editA.add(ImmutableList.of(edit.getBeginA(), edit.getEndA() - edit.getBeginA())); + } + if (edit.getBeginB() != edit.getEndB()) { + e.editB.add(ImmutableList.of(edit.getBeginB(), edit.getEndB() - edit.getBeginB())); + } + } + } + } + + 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; + } + } + + static final class ContentEntry { + // Common lines to both sides. + List ab; + // Lines of a. + List a; + // Lines of b. + List b; + + // A list of changed sections of the of the corresponding line list. + // Each entry is a character pair. The offset is from the + // beginning of the first line in the list. Also, the offset includes an + // implied trailing newline character for each line. + List> editA; + List> editB; + + // Number of lines to skip on both sides. + Integer skip; + } + + public static class ContextOptionHandler extends OptionHandler { + public ContextOptionHandler( + CmdLineParser parser, OptionDef option, Setter 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"; + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java index e088e4b69b..4c87d98070 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java @@ -85,6 +85,7 @@ public class Module extends RestApiModule { put(FILE_KIND, "reviewed").to(PutReviewed.class); delete(FILE_KIND, "reviewed").to(DeleteReviewed.class); get(FILE_KIND, "content").to(GetContent.class); + get(FILE_KIND, "diff").to(GetDiff.class); install(new FactoryModule() { @Override diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java index ff9e6cf9ff..852165c28c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java @@ -171,7 +171,10 @@ public class PatchListEntry { final List headerLines = new ArrayList(m.size() - 1); for (int i = 1; i < m.size() - 1; i++) { final int b = m.get(i); - final int e = m.get(i + 1); + int e = m.get(i + 1); + if (header[e - 1] == '\n') { + e--; + } headerLines.add(RawParseUtils.decode(Constants.CHARSET, header, b, e)); } return headerLines; diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java index eaa5e75998..e74d5a192f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java @@ -211,7 +211,8 @@ class PatchScriptBuilder { return new PatchScript(change.getKey(), content.getChangeType(), content.getOldName(), content.getNewName(), a.fileMode, b.fileMode, content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits, - a.displayMethod, b.displayMethod, comments, history, hugeFile, + a.displayMethod, b.displayMethod, a.mimeType.toString(), + b.mimeType.toString(), comments, history, hugeFile, intralineDifferenceIsPossible, intralineFailure, intralineTimeout); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java index e700f1450f..b46f1e7664 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java @@ -56,7 +56,9 @@ import javax.annotation.Nullable; public class PatchScriptFactory implements Callable { public interface Factory { - PatchScriptFactory create(Patch.Key patchKey, + PatchScriptFactory create( + ChangeControl control, + String fileName, @Assisted("patchSetA") PatchSet.Id patchSetA, @Assisted("patchSetB") PatchSet.Id patchSetB, AccountDiffPreference diffPrefs); @@ -69,20 +71,17 @@ public class PatchScriptFactory implements Callable { private final Provider builderFactory; private final PatchListCache patchListCache; private final ReviewDb db; - private final ChangeControl.Factory changeControlFactory; private final AccountInfoCacheFactory.Factory aicFactory; - private final Patch.Key patchKey; + private final String fileName; @Nullable private final PatchSet.Id psa; private final PatchSet.Id psb; private final AccountDiffPreference diffPrefs; - private final PatchSet.Id patchSetId; private final Change.Id changeId; private Change change; - private PatchSet patchSet; private Project.NameKey projectKey; private ChangeControl control; private ObjectId aId; @@ -94,9 +93,9 @@ public class PatchScriptFactory implements Callable { PatchScriptFactory(final GitRepositoryManager grm, Provider builderFactory, final PatchListCache patchListCache, final ReviewDb db, - final ChangeControl.Factory changeControlFactory, final AccountInfoCacheFactory.Factory aicFactory, - @Assisted final Patch.Key patchKey, + @Assisted ChangeControl control, + @Assisted final String fileName, @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA, @Assisted("patchSetB") final PatchSet.Id patchSetB, @Assisted final AccountDiffPreference diffPrefs) { @@ -104,16 +103,15 @@ public class PatchScriptFactory implements Callable { this.builderFactory = builderFactory; this.patchListCache = patchListCache; this.db = db; - this.changeControlFactory = changeControlFactory; + this.control = control; this.aicFactory = aicFactory; - this.patchKey = patchKey; + this.fileName = fileName; this.psa = patchSetA; this.psb = patchSetB; this.diffPrefs = diffPrefs; - patchSetId = patchKey.getParentKey(); - changeId = patchSetId.getParentKey(); + changeId = patchSetB.getParentKey(); } @Override @@ -122,13 +120,8 @@ public class PatchScriptFactory implements Callable { validatePatchSetId(psa); validatePatchSetId(psb); - control = changeControlFactory.validateFor(changeId); change = control.getChange(); projectKey = change.getProject(); - patchSet = db.patchSets().get(patchSetId); - if (patchSet == null) { - throw new NoSuchChangeException(changeId); - } aId = psa != null ? toObjectId(db, psa) : null; bId = toObjectId(db, psb); @@ -146,7 +139,7 @@ public class PatchScriptFactory implements Callable { try { final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace())); final PatchScriptBuilder b = newBuilder(list, git); - final PatchListEntry content = list.get(patchKey.getFileName()); + final PatchListEntry content = list.get(fileName); loadCommentsAndHistory(content.getChangeType(), // content.getOldName(), // @@ -227,7 +220,7 @@ public class PatchScriptFactory implements Callable { // proper rename detection between the patch sets. // for (final PatchSet ps : db.patchSets().byChange(changeId)) { - String name = patchKey.get(); + String name = fileName; if (psa != null) { switch (changeType) { case COPIED: