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:
Shawn O. Pearce 2010-01-30 19:41:17 -08:00
parent 0e4f33b82f
commit 7dafe19aee
14 changed files with 482 additions and 45 deletions

View File

@ -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));
}

View File

@ -17,5 +17,6 @@
<source path='diff' includes='
Edit.java
Edit_JsonSerializer.java
ReplaceEdit.java
'/>
</module>

View File

@ -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;
}
}

View File

@ -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]; }-*/;
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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 &#39; 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>");

View File

@ -23,7 +23,7 @@ public class PrettySettings {
protected boolean showTabs;
public PrettySettings() {
showWhiteSpaceErrors = false;
showWhiteSpaceErrors = true;
lineLength = 100;
tabSize = 2;
showTabs = true;

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}
}