HunkHeader.java
/*
* Copyright (C) 2008-2009, Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.patch;
import static org.eclipse.jgit.util.RawParseUtils.match;
import static org.eclipse.jgit.util.RawParseUtils.nextLF;
import static org.eclipse.jgit.util.RawParseUtils.parseBase10;
import java.io.IOException;
import java.io.OutputStream;
import java.text.MessageFormat;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.util.MutableInteger;
/**
* Hunk header describing the layout of a single block of lines
*/
public class HunkHeader {
/** Details about an old image of the file. */
public abstract static class OldImage {
/** First line number the hunk starts on in this file. */
int startLine;
/** Total number of lines this hunk covers in this file. */
int lineCount;
/** Number of lines deleted by the post-image from this file. */
int nDeleted;
/** Number of lines added by the post-image not in this file. */
int nAdded;
/** @return first line number the hunk starts on in this file. */
public int getStartLine() {
return startLine;
}
/** @return total number of lines this hunk covers in this file. */
public int getLineCount() {
return lineCount;
}
/** @return number of lines deleted by the post-image from this file. */
public int getLinesDeleted() {
return nDeleted;
}
/** @return number of lines added by the post-image not in this file. */
public int getLinesAdded() {
return nAdded;
}
/** @return object id of the pre-image file. */
public abstract AbbreviatedObjectId getId();
}
final FileHeader file;
/** Offset within {@link #file}.buf to the "@@ -" line. */
final int startOffset;
/** Position 1 past the end of this hunk within {@link #file}'s buf. */
int endOffset;
private final OldImage old;
/** First line number in the post-image file where the hunk starts */
int newStartLine;
/** Total number of post-image lines this hunk covers (context + inserted) */
int newLineCount;
/** Total number of lines of context appearing in this hunk */
int nContext;
private EditList editList;
HunkHeader(FileHeader fh, int offset) {
this(fh, offset, new OldImage() {
@Override
public AbbreviatedObjectId getId() {
return fh.getOldId();
}
});
}
HunkHeader(FileHeader fh, int offset, OldImage oi) {
file = fh;
startOffset = offset;
old = oi;
}
HunkHeader(FileHeader fh, EditList editList) {
this(fh, fh.buf.length);
this.editList = editList;
endOffset = startOffset;
nContext = 0;
if (editList.isEmpty()) {
newStartLine = 0;
newLineCount = 0;
} else {
newStartLine = editList.get(0).getBeginB();
Edit last = editList.get(editList.size() - 1);
newLineCount = last.getEndB() - newStartLine;
}
}
/**
* Get header for the file this hunk applies to.
*
* @return header for the file this hunk applies to.
*/
public FileHeader getFileHeader() {
return file;
}
/**
* Get the byte array holding this hunk's patch script.
*
* @return the byte array holding this hunk's patch script.
*/
public byte[] getBuffer() {
return file.buf;
}
/**
* Get offset of the start of this hunk in {@link #getBuffer()}.
*
* @return offset of the start of this hunk in {@link #getBuffer()}.
*/
public int getStartOffset() {
return startOffset;
}
/**
* Get offset one past the end of the hunk in {@link #getBuffer()}.
*
* @return offset one past the end of the hunk in {@link #getBuffer()}.
*/
public int getEndOffset() {
return endOffset;
}
/**
* Get information about the old image mentioned in this hunk.
*
* @return information about the old image mentioned in this hunk.
*/
public OldImage getOldImage() {
return old;
}
/**
* Get first line number in the post-image file where the hunk starts.
*
* @return first line number in the post-image file where the hunk starts.
*/
public int getNewStartLine() {
return newStartLine;
}
/**
* Get total number of post-image lines this hunk covers.
*
* @return total number of post-image lines this hunk covers.
*/
public int getNewLineCount() {
return newLineCount;
}
/**
* Get total number of lines of context appearing in this hunk.
*
* @return total number of lines of context appearing in this hunk.
*/
public int getLinesContext() {
return nContext;
}
/**
* Convert to a list describing the content edits performed within the hunk.
*
* @return a list describing the content edits performed within the hunk.
*/
public EditList toEditList() {
if (editList == null) {
editList = new EditList();
final byte[] buf = file.buf;
int c = nextLF(buf, startOffset);
int oLine = old.startLine;
int nLine = newStartLine;
Edit in = null;
SCAN: for (; c < endOffset; c = nextLF(buf, c)) {
switch (buf[c]) {
case ' ':
case '\n':
in = null;
oLine++;
nLine++;
continue;
case '-':
if (in == null) {
in = new Edit(oLine - 1, nLine - 1);
editList.add(in);
}
oLine++;
in.extendA();
continue;
case '+':
if (in == null) {
in = new Edit(oLine - 1, nLine - 1);
editList.add(in);
}
nLine++;
in.extendB();
continue;
case '\\': // Matches "\ No newline at end of file"
continue;
default:
break SCAN;
}
}
}
return editList;
}
void parseHeader() {
// Parse "@@ -236,9 +236,9 @@ protected boolean"
//
final byte[] buf = file.buf;
final MutableInteger ptr = new MutableInteger();
ptr.value = nextLF(buf, startOffset, ' ');
old.startLine = -parseBase10(buf, ptr.value, ptr);
if (buf[ptr.value] == ',')
old.lineCount = parseBase10(buf, ptr.value + 1, ptr);
else
old.lineCount = 1;
newStartLine = parseBase10(buf, ptr.value + 1, ptr);
if (buf[ptr.value] == ',')
newLineCount = parseBase10(buf, ptr.value + 1, ptr);
else
newLineCount = 1;
}
int parseBody(Patch script, int end) {
final byte[] buf = file.buf;
int c = nextLF(buf, startOffset), last = c;
old.nDeleted = 0;
old.nAdded = 0;
SCAN: for (; c < end; last = c, c = nextLF(buf, c)) {
switch (buf[c]) {
case ' ':
case '\n':
nContext++;
continue;
case '-':
old.nDeleted++;
continue;
case '+':
old.nAdded++;
continue;
case '\\': // Matches "\ No newline at end of file"
continue;
default:
break SCAN;
}
}
if (last < end && nContext + old.nDeleted - 1 == old.lineCount
&& nContext + old.nAdded == newLineCount
&& match(buf, last, Patch.SIG_FOOTER) >= 0) {
// This is an extremely common occurrence of "corruption".
// Users add footers with their signatures after this mark,
// and git diff adds the git executable version number.
// Let it slide; the hunk otherwise looked sound.
//
old.nDeleted--;
return last;
}
if (nContext + old.nDeleted < old.lineCount) {
final int missingCount = old.lineCount - (nContext + old.nDeleted);
script.error(buf, startOffset, MessageFormat.format(
JGitText.get().truncatedHunkOldLinesMissing,
Integer.valueOf(missingCount)));
} else if (nContext + old.nAdded < newLineCount) {
final int missingCount = newLineCount - (nContext + old.nAdded);
script.error(buf, startOffset, MessageFormat.format(
JGitText.get().truncatedHunkNewLinesMissing,
Integer.valueOf(missingCount)));
} else if (nContext + old.nDeleted > old.lineCount
|| nContext + old.nAdded > newLineCount) {
final String oldcnt = old.lineCount + ":" + newLineCount; //$NON-NLS-1$
final String newcnt = (nContext + old.nDeleted) + ":" //$NON-NLS-1$
+ (nContext + old.nAdded);
script.warn(buf, startOffset, MessageFormat.format(
JGitText.get().hunkHeaderDoesNotMatchBodyLineCountOf, oldcnt, newcnt));
}
return c;
}
void extractFileLines(OutputStream[] out) throws IOException {
final byte[] buf = file.buf;
int ptr = startOffset;
int eol = nextLF(buf, ptr);
if (endOffset <= eol)
return;
// Treat the hunk header as though it were from the ancestor,
// as it may have a function header appearing after it which
// was copied out of the ancestor file.
//
out[0].write(buf, ptr, eol - ptr);
SCAN: for (ptr = eol; ptr < endOffset; ptr = eol) {
eol = nextLF(buf, ptr);
switch (buf[ptr]) {
case ' ':
case '\n':
case '\\':
out[0].write(buf, ptr, eol - ptr);
out[1].write(buf, ptr, eol - ptr);
break;
case '-':
out[0].write(buf, ptr, eol - ptr);
break;
case '+':
out[1].write(buf, ptr, eol - ptr);
break;
default:
break SCAN;
}
}
}
void extractFileLines(final StringBuilder sb, final String[] text,
final int[] offsets) {
final byte[] buf = file.buf;
int ptr = startOffset;
int eol = nextLF(buf, ptr);
if (endOffset <= eol)
return;
copyLine(sb, text, offsets, 0);
SCAN: for (ptr = eol; ptr < endOffset; ptr = eol) {
eol = nextLF(buf, ptr);
switch (buf[ptr]) {
case ' ':
case '\n':
case '\\':
copyLine(sb, text, offsets, 0);
skipLine(text, offsets, 1);
break;
case '-':
copyLine(sb, text, offsets, 0);
break;
case '+':
copyLine(sb, text, offsets, 1);
break;
default:
break SCAN;
}
}
}
void copyLine(final StringBuilder sb, final String[] text,
final int[] offsets, final int fileIdx) {
final String s = text[fileIdx];
final int start = offsets[fileIdx];
int end = s.indexOf('\n', start);
if (end < 0)
end = s.length();
else
end++;
sb.append(s, start, end);
offsets[fileIdx] = end;
}
void skipLine(final String[] text, final int[] offsets,
final int fileIdx) {
final String s = text[fileIdx];
final int end = s.indexOf('\n', offsets[fileIdx]);
offsets[fileIdx] = end < 0 ? s.length() : end + 1;
}
/** {@inheritDoc} */
@SuppressWarnings("nls")
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("HunkHeader[");
buf.append(getOldImage().getStartLine());
buf.append(',');
buf.append(getOldImage().getLineCount());
buf.append("->");
buf.append(getNewStartLine()).append(',').append(getNewLineCount());
buf.append(']');
return buf.toString();
}
}