Highlight line-level (aka word) differences in files
We now highlight any changed words within a line replace edit, making the actual changes stand out against the surrounding context that makes up the line. The highlight is computed by constructing a string that covers the entire replaced region and then running the Myers diff algorithm over the individual characters of those two regions. To avoid tiny edits interleaved at every other character in a sentance we combine two neighboring character edits together if there are only 1 or 2 characters between them. There are probably many ways to improve on this algorithm to avoid some nasty corner display cases, but this rule is good enough for now. The highlight data is computed and stored as part of the diff cache, which requires a schema change in this commit. So existing diff cache records will be flushed on the next server start, and they will be recomputed on demand. Bug: issue 169 Change-Id: I69142ebef600e8c3c65821272dad3ee04a497654 Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
parent
0e4f33b82f
commit
7dafe19aee
@ -123,10 +123,11 @@ class PatchScriptBuilder {
|
||||
a.path = oldName(contentAct);
|
||||
b.path = newName(contentAct);
|
||||
|
||||
edits = new ArrayList<Edit>(contentAct.getEdits());
|
||||
|
||||
a.resolve(null, aId);
|
||||
b.resolve(a, bId);
|
||||
|
||||
edits = new ArrayList<Edit>(contentAct.getEdits());
|
||||
ensureCommentsVisible(comments);
|
||||
header.addAll(contentAct.getHeaderLines());
|
||||
|
||||
@ -389,9 +390,16 @@ class PatchScriptBuilder {
|
||||
if (!reuse && displayMethod == DisplayMethod.DIFF) {
|
||||
PrettySettings s = new PrettySettings(settings.getPrettySettings());
|
||||
s.setFileName(path);
|
||||
s.setShowWhiteSpaceErrors(other != null /* side B */);
|
||||
|
||||
src = prettyFactory.get();
|
||||
if (other == null /* side A */) {
|
||||
src.setEditFilter(PrettyFormatter.A);
|
||||
s.setShowWhiteSpaceErrors(false);
|
||||
} else {
|
||||
src.setEditFilter(PrettyFormatter.B);
|
||||
s.setShowWhiteSpaceErrors(s.isShowWhiteSpaceErrors());
|
||||
}
|
||||
src.setEditList(edits);
|
||||
src.format(s, Text.asString(srcContent, null));
|
||||
}
|
||||
|
||||
|
@ -17,5 +17,6 @@
|
||||
<source path='diff' includes='
|
||||
Edit.java
|
||||
Edit_JsonSerializer.java
|
||||
ReplaceEdit.java
|
||||
'/>
|
||||
</module>
|
||||
|
@ -25,6 +25,8 @@ import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EditDeserializer implements JsonDeserializer<Edit>,
|
||||
JsonSerializer<Edit> {
|
||||
@ -34,14 +36,28 @@ public class EditDeserializer implements JsonDeserializer<Edit>,
|
||||
return null;
|
||||
}
|
||||
if (!json.isJsonArray()) {
|
||||
throw new JsonParseException("Expected array of 4for Edit type");
|
||||
throw new JsonParseException("Expected array for Edit type");
|
||||
}
|
||||
|
||||
final JsonArray a = (JsonArray) json;
|
||||
if (a.size() != 4) {
|
||||
final JsonArray o = (JsonArray) json;
|
||||
final int cnt = o.size();
|
||||
if (cnt < 4 || cnt % 4 != 0) {
|
||||
throw new JsonParseException("Expected array of 4 for Edit type");
|
||||
}
|
||||
return new Edit(get(a, 0), get(a, 1), get(a, 2), get(a, 3));
|
||||
|
||||
if (4 == cnt) {
|
||||
return new Edit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
|
||||
}
|
||||
|
||||
List<Edit> l = new ArrayList<Edit>((cnt / 4) - 1);
|
||||
for (int i = 4; i < cnt;) {
|
||||
int as = get(o, i++);
|
||||
int ae = get(o, i++);
|
||||
int bs = get(o, i++);
|
||||
int be = get(o, i++);
|
||||
l.add(new Edit(as, ae, bs, be));
|
||||
}
|
||||
return new ReplaceEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l);
|
||||
}
|
||||
|
||||
private static int get(final JsonArray a, final int idx)
|
||||
@ -63,10 +79,19 @@ public class EditDeserializer implements JsonDeserializer<Edit>,
|
||||
return new JsonNull();
|
||||
}
|
||||
final JsonArray a = new JsonArray();
|
||||
add(a, src);
|
||||
if (src instanceof ReplaceEdit) {
|
||||
for (Edit e : ((ReplaceEdit) src).getInternalEdits()) {
|
||||
add(a, e);
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private void add(final JsonArray a, final Edit src) {
|
||||
a.add(new JsonPrimitive(src.getBeginA()));
|
||||
a.add(new JsonPrimitive(src.getEndA()));
|
||||
a.add(new JsonPrimitive(src.getBeginB()));
|
||||
a.add(new JsonPrimitive(src.getEndB()));
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ package org.eclipse.jgit.diff;
|
||||
import com.google.gwt.core.client.JavaScriptObject;
|
||||
import com.google.gwtjsonrpc.client.impl.JsonSerializer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Edit_JsonSerializer extends JsonSerializer<Edit> {
|
||||
public static final Edit_JsonSerializer INSTANCE = new Edit_JsonSerializer();
|
||||
|
||||
@ -25,13 +28,38 @@ public class Edit_JsonSerializer extends JsonSerializer<Edit> {
|
||||
if (jso == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final JavaScriptObject o = (JavaScriptObject) jso;
|
||||
return new Edit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
|
||||
final int cnt = length(o);
|
||||
if (4 == cnt) {
|
||||
return new Edit(get(o, 0), get(o, 1), get(o, 2), get(o, 3));
|
||||
}
|
||||
|
||||
List<Edit> l = new ArrayList<Edit>((cnt / 4) - 1);
|
||||
for (int i = 4; i < cnt;) {
|
||||
int as = get(o, i++);
|
||||
int ae = get(o, i++);
|
||||
int bs = get(o, i++);
|
||||
int be = get(o, i++);
|
||||
l.add(new Edit(as, ae, bs, be));
|
||||
}
|
||||
return new ReplaceEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJson(final StringBuilder sb, final Edit o) {
|
||||
sb.append('[');
|
||||
append(sb, o);
|
||||
if (o instanceof ReplaceEdit) {
|
||||
for (Edit e : ((ReplaceEdit) o).getInternalEdits()) {
|
||||
sb.append(',');
|
||||
append(sb, e);
|
||||
}
|
||||
}
|
||||
sb.append(']');
|
||||
}
|
||||
|
||||
private void append(final StringBuilder sb, final Edit o) {
|
||||
sb.append(o.getBeginA());
|
||||
sb.append(',');
|
||||
sb.append(o.getEndA());
|
||||
@ -39,9 +67,11 @@ public class Edit_JsonSerializer extends JsonSerializer<Edit> {
|
||||
sb.append(o.getBeginB());
|
||||
sb.append(',');
|
||||
sb.append(o.getEndB());
|
||||
sb.append(']');
|
||||
}
|
||||
|
||||
private static native int length(JavaScriptObject jso)
|
||||
/*-{ return jso.length; }-*/;
|
||||
|
||||
private static native int get(JavaScriptObject jso, int idx)
|
||||
/*-{ return jso[idx]; }-*/;
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
// Copyright (C) 2010 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 org.eclipse.jgit.diff;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ReplaceEdit extends Edit {
|
||||
private List<Edit> internalEdit;
|
||||
|
||||
public ReplaceEdit(int as, int ae, int bs, int be, List<Edit> internal) {
|
||||
super(as, ae, bs, be);
|
||||
internalEdit = internal;
|
||||
}
|
||||
|
||||
public ReplaceEdit(Edit orig, List<Edit> internal) {
|
||||
super(orig.getBeginA(), orig.getEndA(), orig.getBeginB(), orig.getEndB());
|
||||
internalEdit = internal;
|
||||
}
|
||||
|
||||
public List<Edit> getInternalEdits() {
|
||||
return internalEdit;
|
||||
}
|
||||
}
|
@ -48,6 +48,12 @@ limitations under the License.
|
||||
<artifactId>js</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.gerrit</groupId>
|
||||
<artifactId>gerrit-patch-jgit</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.gwt</groupId>
|
||||
<artifactId>gwt-user</artifactId>
|
||||
|
@ -17,14 +17,74 @@ package com.google.gerrit.prettify.common;
|
||||
import com.google.gwtexpui.safehtml.client.SafeHtml;
|
||||
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
|
||||
|
||||
import org.eclipse.jgit.diff.Edit;
|
||||
import org.eclipse.jgit.diff.ReplaceEdit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class PrettyFormatter {
|
||||
public static abstract class EditFilter {
|
||||
protected abstract int getBegin(Edit e);
|
||||
|
||||
protected abstract int getEnd(Edit e);
|
||||
|
||||
protected abstract String getStyleName();
|
||||
|
||||
protected final boolean in(int line, Edit e) {
|
||||
return getBegin(e) <= line && line < getEnd(e);
|
||||
}
|
||||
|
||||
protected final boolean after(int line, Edit e) {
|
||||
return getEnd(e) < line;
|
||||
}
|
||||
}
|
||||
|
||||
public static final EditFilter A = new EditFilter() {
|
||||
@Override
|
||||
protected String getStyleName() {
|
||||
return "wdd";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getBegin(Edit e) {
|
||||
return e.getBeginA();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getEnd(Edit e) {
|
||||
return e.getEndA();
|
||||
}
|
||||
};
|
||||
|
||||
public static final EditFilter B = new EditFilter() {
|
||||
@Override
|
||||
protected String getStyleName() {
|
||||
return "wdi";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getBegin(Edit e) {
|
||||
return e.getBeginB();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getEnd(Edit e) {
|
||||
return e.getEndB();
|
||||
}
|
||||
};
|
||||
|
||||
protected List<String> lines = Collections.emptyList();
|
||||
protected EditFilter side = A;
|
||||
protected List<Edit> lineEdits = Collections.emptyList();
|
||||
protected PrettySettings settings;
|
||||
|
||||
private int col;
|
||||
private int line;
|
||||
private Tag lastTag;
|
||||
private StringBuilder buf;
|
||||
|
||||
/** @return the line of formatted HTML. */
|
||||
public SafeHtml getLine(int lineNo) {
|
||||
return SafeHtml.asis(lines.get(lineNo));
|
||||
@ -35,6 +95,14 @@ public abstract class PrettyFormatter {
|
||||
return lines.size();
|
||||
}
|
||||
|
||||
public void setEditFilter(EditFilter f) {
|
||||
side = f;
|
||||
}
|
||||
|
||||
public void setEditList(List<Edit> all) {
|
||||
lineEdits = all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and format a complete source code file.
|
||||
*
|
||||
@ -49,10 +117,12 @@ public abstract class PrettyFormatter {
|
||||
String html = prettify(toHTML(srcText));
|
||||
int pos = 0;
|
||||
int textChunkStart = 0;
|
||||
int col = 0;
|
||||
Tag lastTag = Tag.NULL;
|
||||
|
||||
StringBuilder buf = new StringBuilder();
|
||||
lastTag = Tag.NULL;
|
||||
col = 0;
|
||||
line = 0;
|
||||
|
||||
buf = new StringBuilder();
|
||||
while (pos <= html.length()) {
|
||||
int tagStart = html.indexOf('<', pos);
|
||||
|
||||
@ -62,7 +132,7 @@ public abstract class PrettyFormatter {
|
||||
assert lastTag == Tag.NULL;
|
||||
pos = html.length();
|
||||
if (textChunkStart < pos) {
|
||||
col = htmlText(col, buf, html.substring(textChunkStart, pos));
|
||||
htmlText(html.substring(textChunkStart, pos));
|
||||
}
|
||||
if (0 < buf.length()) {
|
||||
lines.add(buf.toString());
|
||||
@ -82,7 +152,7 @@ public abstract class PrettyFormatter {
|
||||
//
|
||||
if (textChunkStart < tagStart) {
|
||||
lastTag.open(buf, html);
|
||||
col = htmlText(col, buf, html.substring(textChunkStart, tagStart));
|
||||
htmlText(html.substring(textChunkStart, tagStart));
|
||||
}
|
||||
textChunkStart = pos;
|
||||
|
||||
@ -91,6 +161,7 @@ public abstract class PrettyFormatter {
|
||||
lines.add(buf.toString());
|
||||
buf = new StringBuilder();
|
||||
col = 0;
|
||||
line++;
|
||||
|
||||
} else if (html.charAt(tagStart + 1) == '/') {
|
||||
lastTag = lastTag.pop(buf, html);
|
||||
@ -99,18 +170,18 @@ public abstract class PrettyFormatter {
|
||||
lastTag = new Tag(lastTag, tagStart, tagEnd);
|
||||
}
|
||||
}
|
||||
buf = null;
|
||||
}
|
||||
|
||||
private int htmlText(int col, StringBuilder buf, String txt) {
|
||||
private void htmlText(String txt) {
|
||||
int pos = 0;
|
||||
|
||||
while (pos < txt.length()) {
|
||||
int start = txt.indexOf('&', pos);
|
||||
if (start < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
col = cleanText(col, buf, txt, pos, start);
|
||||
cleanText(txt, pos, start);
|
||||
pos = txt.indexOf(';', start + 1) + 1;
|
||||
|
||||
if (settings.getLineLength() <= col) {
|
||||
@ -122,10 +193,10 @@ public abstract class PrettyFormatter {
|
||||
col++;
|
||||
}
|
||||
|
||||
return cleanText(col, buf, txt, pos, txt.length());
|
||||
cleanText(txt, pos, txt.length());
|
||||
}
|
||||
|
||||
private int cleanText(int col, StringBuilder buf, String txt, int pos, int end) {
|
||||
private void cleanText(String txt, int pos, int end) {
|
||||
while (pos < end) {
|
||||
int free = settings.getLineLength() - col;
|
||||
if (free <= 0) {
|
||||
@ -142,7 +213,6 @@ public abstract class PrettyFormatter {
|
||||
col += n;
|
||||
pos += n;
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
/** Run the prettify engine over the text and return the result. */
|
||||
@ -212,7 +282,7 @@ public abstract class PrettyFormatter {
|
||||
}
|
||||
|
||||
private String toHTML(String src) {
|
||||
SafeHtml html = new SafeHtmlBuilder().append(src);
|
||||
SafeHtml html = colorLineEdits(src);
|
||||
|
||||
// The prettify parsers don't like ' as an entity for the
|
||||
// single quote character. Replace them all out so we don't
|
||||
@ -236,6 +306,74 @@ public abstract class PrettyFormatter {
|
||||
return html.asString();
|
||||
}
|
||||
|
||||
private SafeHtml colorLineEdits(String src) {
|
||||
SafeHtmlBuilder buf = new SafeHtmlBuilder();
|
||||
|
||||
int lIdx = 0;
|
||||
Edit lCur = lIdx < lineEdits.size() ? lineEdits.get(lIdx) : null;
|
||||
|
||||
int pos = 0;
|
||||
int line = 0;
|
||||
while (pos < src.length()) {
|
||||
if (lCur instanceof ReplaceEdit && side.in(line, lCur)) {
|
||||
List<Edit> wordEdits = ((ReplaceEdit) lCur).getInternalEdits();
|
||||
if (!wordEdits.isEmpty()) {
|
||||
// Copy the result using the word edits to guide us.
|
||||
//
|
||||
|
||||
int last = 0;
|
||||
for (Edit w : wordEdits) {
|
||||
int b = side.getBegin(w);
|
||||
int e = side.getEnd(w);
|
||||
|
||||
// If there is text between edits, copy it as-is.
|
||||
//
|
||||
int cnt = b - last;
|
||||
if (0 < cnt) {
|
||||
buf.append(src.substring(pos, pos + cnt));
|
||||
pos += cnt;
|
||||
last = b;
|
||||
}
|
||||
|
||||
// If this is an edit, wrap it in a span.
|
||||
//
|
||||
cnt = e - b;
|
||||
if (0 < cnt) {
|
||||
buf.openSpan();
|
||||
buf.setStyleName(side.getStyleName());
|
||||
buf.append(src.substring(pos, pos + cnt));
|
||||
buf.closeSpan();
|
||||
pos += cnt;
|
||||
last = e;
|
||||
}
|
||||
}
|
||||
|
||||
// We've consumed the entire region, so we are on the end.
|
||||
// Fall through, what's left of this edit is only the tail
|
||||
// of the final line.
|
||||
//
|
||||
line = side.getEnd(lCur) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
int lf = src.indexOf('\n', pos);
|
||||
if (lf < 0)
|
||||
lf = src.length();
|
||||
else
|
||||
lf++;
|
||||
|
||||
buf.append(src.substring(pos, lf));
|
||||
pos = lf;
|
||||
line++;
|
||||
|
||||
if (lCur != null && side.after(line, lCur)) {
|
||||
lIdx++;
|
||||
lCur = lIdx < lineEdits.size() ? lineEdits.get(lIdx) : null;
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private SafeHtml showTabAfterSpace(SafeHtml src) {
|
||||
src = src.replaceFirst("^( *\t)", "<span class=\"wse\">$1</span>");
|
||||
src = src.replaceAll("\n( *\t)", "\n<span class=\"wse\">$1</span>");
|
||||
|
@ -23,7 +23,7 @@ public class PrettySettings {
|
||||
protected boolean showTabs;
|
||||
|
||||
public PrettySettings() {
|
||||
showWhiteSpaceErrors = false;
|
||||
showWhiteSpaceErrors = true;
|
||||
lineLength = 100;
|
||||
tabSize = 2;
|
||||
showTabs = true;
|
||||
|
@ -15,6 +15,8 @@
|
||||
|
||||
@external .wse;
|
||||
@external .vt;
|
||||
@external .wdd;
|
||||
@external .wdi;
|
||||
|
||||
.wse {
|
||||
background: red;
|
||||
@ -27,3 +29,10 @@
|
||||
.wse .vt {
|
||||
border-left: 2px dotted black;
|
||||
}
|
||||
|
||||
.wdd {
|
||||
background: #FAA;
|
||||
}
|
||||
.wdi {
|
||||
background: #9F9;
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
// Copyright (C) 2010 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.patch;
|
||||
|
||||
import org.eclipse.jgit.diff.Sequence;
|
||||
|
||||
class CharText implements Sequence {
|
||||
private final String content;
|
||||
|
||||
CharText(Text text, int s, int e) {
|
||||
content = text.getLines(s, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(int a, Sequence other, int b) {
|
||||
return content.charAt(a) == ((CharText) other).content.charAt(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return content.length();
|
||||
}
|
||||
}
|
@ -31,18 +31,28 @@ import com.google.inject.Singleton;
|
||||
import com.google.inject.TypeLiteral;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
import org.eclipse.jgit.diff.Edit;
|
||||
import org.eclipse.jgit.diff.MyersDiff;
|
||||
import org.eclipse.jgit.diff.ReplaceEdit;
|
||||
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
|
||||
import org.eclipse.jgit.errors.MissingObjectException;
|
||||
import org.eclipse.jgit.lib.AnyObjectId;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectLoader;
|
||||
import org.eclipse.jgit.lib.ObjectWriter;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.patch.FileHeader;
|
||||
import org.eclipse.jgit.patch.FileHeader.PatchType;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevTree;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Provides a cached list of {@link PatchListEntry}. */
|
||||
@ -109,7 +119,8 @@ public class PatchListCacheImpl implements PatchListCache {
|
||||
|
||||
private PatchList readPatchList(final PatchListKey key, final Repository repo)
|
||||
throws IOException {
|
||||
final RevCommit b = new RevWalk(repo).parseCommit(key.getNewId());
|
||||
final RevWalk rw = new RevWalk(repo);
|
||||
final RevCommit b = rw.parseCommit(key.getNewId());
|
||||
final AnyObjectId a = aFor(key, repo, b);
|
||||
|
||||
final List<String> args = new ArrayList<String>();
|
||||
@ -163,14 +174,94 @@ public class PatchListCacheImpl implements PatchListCache {
|
||||
}
|
||||
}
|
||||
|
||||
RevTree aTree = a != null ? rw.parseCommit(a).getTree() : null;
|
||||
RevTree bTree = b.getTree();
|
||||
|
||||
final int cnt = p.getFiles().size();
|
||||
final PatchListEntry[] entries = new PatchListEntry[cnt];
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
entries[i] = new PatchListEntry(p.getFiles().get(i));
|
||||
entries[i] = newEntry(repo, aTree, bTree, p.getFiles().get(i));
|
||||
}
|
||||
return new PatchList(a, b, entries);
|
||||
}
|
||||
|
||||
private static PatchListEntry newEntry(Repository repo, RevTree aTree,
|
||||
RevTree bTree, FileHeader fileHeader) throws IOException {
|
||||
if (fileHeader.getHunks().isEmpty()) {
|
||||
return new PatchListEntry(fileHeader, Collections.<Edit> emptyList());
|
||||
}
|
||||
|
||||
List<Edit> edits = fileHeader.toEditList();
|
||||
|
||||
// Bypass the longer task of looking for replacement edits if
|
||||
// there cannot be a replacement within plain text.
|
||||
//
|
||||
if (aTree == null /* want combined diff */) {
|
||||
return new PatchListEntry(fileHeader, edits);
|
||||
}
|
||||
if (fileHeader.getPatchType() != PatchType.UNIFIED || edits.isEmpty()) {
|
||||
return new PatchListEntry(fileHeader, edits);
|
||||
}
|
||||
switch (fileHeader.getChangeType()) {
|
||||
case ADD:
|
||||
case DELETE:
|
||||
return new PatchListEntry(fileHeader, edits);
|
||||
}
|
||||
|
||||
Text aContent = null;
|
||||
Text bContent = null;
|
||||
|
||||
for (int i = 0; i < edits.size(); i++) {
|
||||
Edit e = edits.get(i);
|
||||
|
||||
if (e.getType() == Edit.Type.REPLACE) {
|
||||
if (aContent == null) {
|
||||
edits = new ArrayList<Edit>(edits);
|
||||
aContent = read(repo, fileHeader.getOldName(), aTree);
|
||||
bContent = read(repo, fileHeader.getNewName(), bTree);
|
||||
}
|
||||
|
||||
CharText a = new CharText(aContent, e.getBeginA(), e.getEndA());
|
||||
CharText b = new CharText(bContent, e.getBeginB(), e.getEndB());
|
||||
|
||||
List<Edit> wordEdits = new MyersDiff(a, b).getEdits();
|
||||
for (int j = 0; j < wordEdits.size() - 1;) {
|
||||
Edit c = wordEdits.get(j);
|
||||
Edit n = wordEdits.get(j + 1);
|
||||
|
||||
if (n.getBeginA() - c.getEndA() <= 2
|
||||
|| n.getBeginB() - c.getEndB() <= 2) {
|
||||
// This edit is incredibly close to the start of the next.
|
||||
// Combine them together.
|
||||
//
|
||||
wordEdits.set(j, new Edit(c.getBeginA(), n.getEndA(),
|
||||
c.getBeginB(), n.getEndB()));
|
||||
wordEdits.remove(j + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
j++;
|
||||
}
|
||||
edits.set(i, new ReplaceEdit(e, wordEdits));
|
||||
}
|
||||
}
|
||||
|
||||
return new PatchListEntry(fileHeader, edits);
|
||||
}
|
||||
|
||||
private static Text read(Repository repo, String path, RevTree tree)
|
||||
throws IOException {
|
||||
TreeWalk tw = TreeWalk.forPath(repo, path, tree);
|
||||
if (tw == null || tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) {
|
||||
return Text.EMPTY;
|
||||
}
|
||||
ObjectLoader ldr = repo.openObject(tw.getObjectId(0));
|
||||
if (ldr == null) {
|
||||
return Text.EMPTY;
|
||||
}
|
||||
return new Text(ldr.getCachedBytes());
|
||||
}
|
||||
|
||||
private static AnyObjectId aFor(final PatchListKey key,
|
||||
final Repository repo, final RevCommit b) throws IOException {
|
||||
if (key.getOldId() != null) {
|
||||
|
@ -29,6 +29,7 @@ import com.google.gerrit.reviewdb.Patch.ChangeType;
|
||||
import com.google.gerrit.reviewdb.Patch.PatchType;
|
||||
|
||||
import org.eclipse.jgit.diff.Edit;
|
||||
import org.eclipse.jgit.diff.ReplaceEdit;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.FileMode;
|
||||
import org.eclipse.jgit.patch.CombinedFileHeader;
|
||||
@ -59,7 +60,7 @@ public class PatchListEntry {
|
||||
private final byte[] header;
|
||||
private final List<Edit> edits;
|
||||
|
||||
PatchListEntry(final FileHeader hdr) {
|
||||
PatchListEntry(final FileHeader hdr, List<Edit> editList) {
|
||||
changeType = toChangeType(hdr);
|
||||
patchType = toPatchType(hdr);
|
||||
|
||||
@ -93,7 +94,7 @@ public class PatchListEntry {
|
||||
|| hdr.getNewMode() == FileMode.GITLINK) {
|
||||
edits = Collections.emptyList();
|
||||
} else {
|
||||
edits = Collections.unmodifiableList(hdr.toEditList());
|
||||
edits = Collections.unmodifiableList(editList);
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,13 +157,27 @@ public class PatchListEntry {
|
||||
|
||||
writeVarInt32(out, edits.size());
|
||||
for (final Edit e : edits) {
|
||||
writeVarInt32(out, e.getBeginA());
|
||||
writeVarInt32(out, e.getEndA());
|
||||
writeVarInt32(out, e.getBeginB());
|
||||
writeVarInt32(out, e.getEndB());
|
||||
write(out, e);
|
||||
|
||||
if (e instanceof ReplaceEdit) {
|
||||
ReplaceEdit r = (ReplaceEdit) e;
|
||||
writeVarInt32(out, r.getInternalEdits().size());
|
||||
for (Edit i : r.getInternalEdits()) {
|
||||
write(out, i);
|
||||
}
|
||||
} else {
|
||||
writeVarInt32(out, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void write(final OutputStream out, final Edit e) throws IOException {
|
||||
writeVarInt32(out, e.getBeginA());
|
||||
writeVarInt32(out, e.getEndA());
|
||||
writeVarInt32(out, e.getBeginB());
|
||||
writeVarInt32(out, e.getEndB());
|
||||
}
|
||||
|
||||
static PatchListEntry readFrom(final InputStream in) throws IOException {
|
||||
final ChangeType changeType = readEnum(in, ChangeType.values());
|
||||
final PatchType patchType = readEnum(in, PatchType.values());
|
||||
@ -173,15 +188,32 @@ public class PatchListEntry {
|
||||
final int editCount = readVarInt32(in);
|
||||
final Edit[] editArray = new Edit[editCount];
|
||||
for (int i = 0; i < editCount; i++) {
|
||||
final int beginA = readVarInt32(in);
|
||||
final int endA = readVarInt32(in);
|
||||
final int beginB = readVarInt32(in);
|
||||
final int endB = readVarInt32(in);
|
||||
editArray[i] = new Edit(beginA, endA, beginB, endB);
|
||||
editArray[i] = readEdit(in);
|
||||
|
||||
int innerCount = readVarInt32(in);
|
||||
if (0 < innerCount) {
|
||||
Edit[] inner = new Edit[innerCount];
|
||||
for (int innerIdx = 0; innerIdx < innerCount; innerIdx++) {
|
||||
inner[innerIdx] = readEdit(in);
|
||||
}
|
||||
editArray[i] = new ReplaceEdit(editArray[i], toList(inner));
|
||||
}
|
||||
}
|
||||
|
||||
return new PatchListEntry(changeType, patchType, oldName, newName, hdr,
|
||||
Collections.unmodifiableList(Arrays.asList(editArray)));
|
||||
toList(editArray));
|
||||
}
|
||||
|
||||
private static List<Edit> toList(Edit[] l) {
|
||||
return Collections.unmodifiableList(Arrays.asList(l));
|
||||
}
|
||||
|
||||
private static Edit readEdit(final InputStream in) throws IOException {
|
||||
final int beginA = readVarInt32(in);
|
||||
final int endA = readVarInt32(in);
|
||||
final int beginB = readVarInt32(in);
|
||||
final int endB = readVarInt32(in);
|
||||
return new Edit(beginA, endA, beginB, endB);
|
||||
}
|
||||
|
||||
private static byte[] compact(final FileHeader h) {
|
||||
|
@ -35,7 +35,7 @@ import java.io.Serializable;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class PatchListKey implements Serializable {
|
||||
static final long serialVersionUID = 9L;
|
||||
static final long serialVersionUID = 10L;
|
||||
|
||||
private transient ObjectId oldId;
|
||||
private transient ObjectId newId;
|
||||
|
@ -15,18 +15,20 @@
|
||||
package com.google.gerrit.server.patch;
|
||||
|
||||
import org.eclipse.jgit.diff.RawText;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.util.RawParseUtils;
|
||||
import org.mozilla.universalchardet.UniversalDetector;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public class Text extends RawText {
|
||||
public static final byte[] NO_BYTES = {};
|
||||
public static final Text EMPTY = new Text(NO_BYTES);
|
||||
|
||||
public static String asString(byte[] content, String encoding)
|
||||
throws UnsupportedEncodingException {
|
||||
public static String asString(byte[] content, String encoding) {
|
||||
return new String(content, charset(content, encoding));
|
||||
}
|
||||
|
||||
private static Charset charset(byte[] content, String encoding) {
|
||||
if (encoding == null) {
|
||||
UniversalDetector d = new UniversalDetector(null);
|
||||
d.handleData(content, 0, content.length);
|
||||
@ -36,9 +38,11 @@ public class Text extends RawText {
|
||||
if (encoding == null) {
|
||||
encoding = "ISO-8859-1";
|
||||
}
|
||||
return new String(content, encoding);
|
||||
return Charset.forName(encoding);
|
||||
}
|
||||
|
||||
private Charset charset;
|
||||
|
||||
public Text(final byte[] r) {
|
||||
super(r);
|
||||
}
|
||||
@ -48,11 +52,34 @@ public class Text extends RawText {
|
||||
}
|
||||
|
||||
public String getLine(final int i) {
|
||||
final int s = lines.get(i + 1);
|
||||
int e = lines.get(i + 2);
|
||||
return getLines(i, i + 1);
|
||||
}
|
||||
|
||||
public String getLines(final int begin, final int end) {
|
||||
if (begin == end) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final int s = getLineStart(begin);
|
||||
int e = getLineEnd(end - 1);
|
||||
if (content[e - 1] == '\n') {
|
||||
e--;
|
||||
}
|
||||
return RawParseUtils.decode(Constants.CHARSET, content, s, e);
|
||||
return decode(s, e);
|
||||
}
|
||||
|
||||
private String decode(final int s, int e) {
|
||||
if (charset == null) {
|
||||
charset = charset(content, null);
|
||||
}
|
||||
return RawParseUtils.decode(charset, content, s, e);
|
||||
}
|
||||
|
||||
private int getLineStart(final int i) {
|
||||
return lines.get(i + 1);
|
||||
}
|
||||
|
||||
private int getLineEnd(final int i) {
|
||||
return lines.get(i + 2);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user