| // 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.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.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; |
| import java.util.concurrent.TimeUnit; |
| |
| public class GetDiff implements RestReadView<FileResource> { |
| private final PatchScriptFactory.Factory patchScriptFactoryFactory; |
| private final Provider<Revisions> 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> revisions) { |
| this.patchScriptFactoryFactory = patchScriptFactoryFactory; |
| this.revisions = revisions; |
| } |
| |
| @Override |
| public Response<Result> apply(FileResource resource) |
| throws ResourceConflictException, ResourceNotFoundException, OrmException { |
| 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); |
| |
| try { |
| 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<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()); |
| |
| 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()); |
| result.metaA.lines = ps.getA().size(); |
| } |
| |
| if (ps.getDisplayMethodB() != DisplayMethod.NONE) { |
| result.metaB = new FileMeta(); |
| result.metaB.name = ps.getNewName(); |
| result.metaB.setContentType(ps.getFileModeB(), ps.getMimeTypeB()); |
| result.metaB.lines = ps.getB().size(); |
| } |
| |
| 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; |
| Response<Result> 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()); |
| } |
| } |
| |
| static class Result { |
| FileMeta metaA; |
| FileMeta metaB; |
| IntraLineStatus intralineStatus; |
| ChangeType changeType; |
| List<String> diffHeader; |
| List<ContentEntry> content; |
| } |
| |
| static class FileMeta { |
| String name; |
| String contentType; |
| Integer lines; |
| |
| 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<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; |
| } |
| } |
| |
| static final class ContentEntry { |
| // Common lines to both sides. |
| List<String> ab; |
| // Lines of a. |
| List<String> a; |
| // Lines of b. |
| List<String> b; |
| |
| // A list of changed sections of the corresponding line list. |
| // Each entry is a character <offset, length> 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<List<Integer>> editA; |
| List<List<Integer>> editB; |
| |
| // a and b are actually common with this whitespace ignore setting. |
| Boolean common; |
| |
| // Number of lines to skip on both sides. |
| Integer skip; |
| } |
| |
| 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"; |
| } |
| } |
| } |