View Javadoc
1   /*
2    * Copyright (C) 2008-2009, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.patch;
45  
46  import static java.nio.charset.StandardCharsets.UTF_8;
47  import static org.eclipse.jgit.lib.Constants.encodeASCII;
48  import static org.eclipse.jgit.util.RawParseUtils.decode;
49  import static org.eclipse.jgit.util.RawParseUtils.decodeNoFallback;
50  import static org.eclipse.jgit.util.RawParseUtils.extractBinaryString;
51  import static org.eclipse.jgit.util.RawParseUtils.match;
52  import static org.eclipse.jgit.util.RawParseUtils.nextLF;
53  import static org.eclipse.jgit.util.RawParseUtils.parseBase10;
54  
55  import java.io.IOException;
56  import java.nio.charset.CharacterCodingException;
57  import java.nio.charset.Charset;
58  import java.text.MessageFormat;
59  import java.util.ArrayList;
60  import java.util.Collections;
61  import java.util.List;
62  
63  import org.eclipse.jgit.diff.DiffEntry;
64  import org.eclipse.jgit.diff.EditList;
65  import org.eclipse.jgit.internal.JGitText;
66  import org.eclipse.jgit.lib.AbbreviatedObjectId;
67  import org.eclipse.jgit.lib.FileMode;
68  import org.eclipse.jgit.util.QuotedString;
69  import org.eclipse.jgit.util.RawParseUtils;
70  import org.eclipse.jgit.util.TemporaryBuffer;
71  
72  /**
73   * Patch header describing an action for a single file path.
74   */
75  public class FileHeader extends DiffEntry {
76  	private static final byte[] OLD_MODE = encodeASCII("old mode "); //$NON-NLS-1$
77  
78  	private static final byte[] NEW_MODE = encodeASCII("new mode "); //$NON-NLS-1$
79  
80  	static final byte[] DELETED_FILE_MODE = encodeASCII("deleted file mode "); //$NON-NLS-1$
81  
82  	static final byte[] NEW_FILE_MODE = encodeASCII("new file mode "); //$NON-NLS-1$
83  
84  	private static final byte[] COPY_FROM = encodeASCII("copy from "); //$NON-NLS-1$
85  
86  	private static final byte[] COPY_TO = encodeASCII("copy to "); //$NON-NLS-1$
87  
88  	private static final byte[] RENAME_OLD = encodeASCII("rename old "); //$NON-NLS-1$
89  
90  	private static final byte[] RENAME_NEW = encodeASCII("rename new "); //$NON-NLS-1$
91  
92  	private static final byte[] RENAME_FROM = encodeASCII("rename from "); //$NON-NLS-1$
93  
94  	private static final byte[] RENAME_TO = encodeASCII("rename to "); //$NON-NLS-1$
95  
96  	private static final byte[] SIMILARITY_INDEX = encodeASCII("similarity index "); //$NON-NLS-1$
97  
98  	private static final byte[] DISSIMILARITY_INDEX = encodeASCII("dissimilarity index "); //$NON-NLS-1$
99  
100 	static final byte[] INDEX = encodeASCII("index "); //$NON-NLS-1$
101 
102 	static final byte[] OLD_NAME = encodeASCII("--- "); //$NON-NLS-1$
103 
104 	static final byte[] NEW_NAME = encodeASCII("+++ "); //$NON-NLS-1$
105 
106 	/** Type of patch used by this file. */
107 	public static enum PatchType {
108 		/** A traditional unified diff style patch of a text file. */
109 		UNIFIED,
110 
111 		/** An empty patch with a message "Binary files ... differ" */
112 		BINARY,
113 
114 		/** A Git binary patch, holding pre and post image deltas */
115 		GIT_BINARY;
116 	}
117 
118 	/** Buffer holding the patch data for this file. */
119 	final byte[] buf;
120 
121 	/** Offset within {@link #buf} to the "diff ..." line. */
122 	final int startOffset;
123 
124 	/** Position 1 past the end of this file within {@link #buf}. */
125 	int endOffset;
126 
127 	/** Type of patch used to modify this file */
128 	PatchType patchType;
129 
130 	/** The hunks of this file */
131 	private List<HunkHeader> hunks;
132 
133 	/** If {@link #patchType} is {@link PatchType#GIT_BINARY}, the new image */
134 	BinaryHunk forwardBinaryHunk;
135 
136 	/** If {@link #patchType} is {@link PatchType#GIT_BINARY}, the old image */
137 	BinaryHunk reverseBinaryHunk;
138 
139 	/**
140 	 * Constructs a new FileHeader
141 	 *
142 	 * @param headerLines
143 	 *            buffer holding the diff header for this file
144 	 * @param edits
145 	 *            the edits for this file
146 	 * @param type
147 	 *            the type of patch used to modify this file
148 	 */
149 	public FileHeader(byte[] headerLines, EditList edits, PatchType type) {
150 		this(headerLines, 0);
151 		endOffset = headerLines.length;
152 		int ptr = parseGitFileName(Patch.DIFF_GIT.length, headerLines.length);
153 		parseGitHeaders(ptr, headerLines.length);
154 		this.patchType = type;
155 		addHunk(new HunkHeader(this, edits));
156 	}
157 
158 	FileHeader(byte[] b, int offset) {
159 		buf = b;
160 		startOffset = offset;
161 		changeType = ChangeType.MODIFY; // unless otherwise designated
162 		patchType = PatchType.UNIFIED;
163 	}
164 
165 	int getParentCount() {
166 		return 1;
167 	}
168 
169 	/**
170 	 * Get the byte array holding this file's patch script.
171 	 *
172 	 * @return the byte array holding this file's patch script.
173 	 */
174 	public byte[] getBuffer() {
175 		return buf;
176 	}
177 
178 	/**
179 	 * Get offset of the start of this file's script in {@link #getBuffer()}.
180 	 *
181 	 * @return offset of the start of this file's script in
182 	 *         {@link #getBuffer()}.
183 	 */
184 	public int getStartOffset() {
185 		return startOffset;
186 	}
187 
188 	/**
189 	 * Get offset one past the end of the file script.
190 	 *
191 	 * @return offset one past the end of the file script.
192 	 */
193 	public int getEndOffset() {
194 		return endOffset;
195 	}
196 
197 	/**
198 	 * Convert the patch script for this file into a string.
199 	 * <p>
200 	 * The default character encoding
201 	 * ({@link java.nio.charset.StandardCharsets#UTF_8}) is assumed for both the
202 	 * old and new files.
203 	 *
204 	 * @return the patch script, as a Unicode string.
205 	 */
206 	public String getScriptText() {
207 		return getScriptText(null, null);
208 	}
209 
210 	/**
211 	 * Convert the patch script for this file into a string.
212 	 *
213 	 * @param oldCharset
214 	 *            hint character set to decode the old lines with.
215 	 * @param newCharset
216 	 *            hint character set to decode the new lines with.
217 	 * @return the patch script, as a Unicode string.
218 	 */
219 	public String getScriptText(Charset oldCharset, Charset newCharset) {
220 		return getScriptText(new Charset[] { oldCharset, newCharset });
221 	}
222 
223 	String getScriptText(Charset[] charsetGuess) {
224 		if (getHunks().isEmpty()) {
225 			// If we have no hunks then we can safely assume the entire
226 			// patch is a binary style patch, or a meta-data only style
227 			// patch. Either way the encoding of the headers should be
228 			// strictly 7-bit US-ASCII and the body is either 7-bit ASCII
229 			// (due to the base 85 encoding used for a BinaryHunk) or is
230 			// arbitrary noise we have chosen to ignore and not understand
231 			// (e.g. the message "Binary files ... differ").
232 			//
233 			return extractBinaryString(buf, startOffset, endOffset);
234 		}
235 
236 		if (charsetGuess != null && charsetGuess.length != getParentCount() + 1)
237 			throw new IllegalArgumentException(MessageFormat.format(
238 					JGitText.get().expectedCharacterEncodingGuesses,
239 					Integer.valueOf(getParentCount() + 1)));
240 
241 		if (trySimpleConversion(charsetGuess)) {
242 			Charset cs = charsetGuess != null ? charsetGuess[0] : null;
243 			if (cs == null) {
244 				cs = UTF_8;
245 			}
246 			try {
247 				return decodeNoFallback(cs, buf, startOffset, endOffset);
248 			} catch (CharacterCodingException cee) {
249 				// Try the much slower, more-memory intensive version which
250 				// can handle a character set conversion patch.
251 			}
252 		}
253 
254 		final StringBuilder r = new StringBuilder(endOffset - startOffset);
255 
256 		// Always treat the headers as US-ASCII; Git file names are encoded
257 		// in a C style escape if any character has the high-bit set.
258 		//
259 		final int hdrEnd = getHunks().get(0).getStartOffset();
260 		for (int ptr = startOffset; ptr < hdrEnd;) {
261 			final int eol = Math.min(hdrEnd, nextLF(buf, ptr));
262 			r.append(extractBinaryString(buf, ptr, eol));
263 			ptr = eol;
264 		}
265 
266 		final String[] files = extractFileLines(charsetGuess);
267 		final int[] offsets = new int[files.length];
268 		for (HunkHeader h : getHunks())
269 			h.extractFileLines(r, files, offsets);
270 		return r.toString();
271 	}
272 
273 	private static boolean trySimpleConversion(Charset[] charsetGuess) {
274 		if (charsetGuess == null)
275 			return true;
276 		for (int i = 1; i < charsetGuess.length; i++) {
277 			if (charsetGuess[i] != charsetGuess[0])
278 				return false;
279 		}
280 		return true;
281 	}
282 
283 	private String[] extractFileLines(Charset[] csGuess) {
284 		final TemporaryBufferr.html#TemporaryBuffer">TemporaryBuffer[] tmp = new TemporaryBuffer[getParentCount() + 1];
285 		try {
286 			for (int i = 0; i < tmp.length; i++)
287 				tmp[i] = new TemporaryBuffer.Heap(Integer.MAX_VALUE);
288 			for (HunkHeader h : getHunks())
289 				h.extractFileLines(tmp);
290 
291 			final String[] r = new String[tmp.length];
292 			for (int i = 0; i < tmp.length; i++) {
293 				Charset cs = csGuess != null ? csGuess[i] : null;
294 				if (cs == null) {
295 					cs = UTF_8;
296 				}
297 				r[i] = RawParseUtils.decode(cs, tmp[i].toByteArray());
298 			}
299 			return r;
300 		} catch (IOException ioe) {
301 			throw new RuntimeException(JGitText.get().cannotConvertScriptToText, ioe);
302 		}
303 	}
304 
305 	/**
306 	 * Get style of patch used to modify this file.
307 	 *
308 	 * @return style of patch used to modify this file.
309 	 */
310 	public PatchType getPatchType() {
311 		return patchType;
312 	}
313 
314 	/**
315 	 * Whether this patch modifies metadata about a file
316 	 *
317 	 * @return {@code true} if this patch modifies metadata about a file .
318 	 */
319 	public boolean hasMetaDataChanges() {
320 		return changeType != ChangeType.MODIFY || newMode != oldMode;
321 	}
322 
323 	/**
324 	 * Get hunks altering this file; in order of appearance in patch
325 	 *
326 	 * @return hunks altering this file; in order of appearance in patch.
327 	 */
328 	public List<? extends HunkHeader> getHunks() {
329 		if (hunks == null)
330 			return Collections.emptyList();
331 		return hunks;
332 	}
333 
334 	void addHunk(HunkHeader h) {
335 		if (h.getFileHeader() != this)
336 			throw new IllegalArgumentException(JGitText.get().hunkBelongsToAnotherFile);
337 		if (hunks == null)
338 			hunks = new ArrayList<>();
339 		hunks.add(h);
340 	}
341 
342 	HunkHeader newHunkHeader(int offset) {
343 		return new HunkHeader(this, offset);
344 	}
345 
346 	/**
347 	 * Get the new-image delta/literal if this is a
348 	 * {@link PatchType#GIT_BINARY}.
349 	 *
350 	 * @return the new-image delta/literal if this is a
351 	 *         {@link PatchType#GIT_BINARY}.
352 	 */
353 	public BinaryHunk getForwardBinaryHunk() {
354 		return forwardBinaryHunk;
355 	}
356 
357 	/**
358 	 * Get the old-image delta/literal if this is a
359 	 * {@link PatchType#GIT_BINARY}.
360 	 *
361 	 * @return the old-image delta/literal if this is a
362 	 *         {@link PatchType#GIT_BINARY}.
363 	 */
364 	public BinaryHunk getReverseBinaryHunk() {
365 		return reverseBinaryHunk;
366 	}
367 
368 	/**
369 	 * Convert to a list describing the content edits performed on this file.
370 	 *
371 	 * @return a list describing the content edits performed on this file.
372 	 */
373 	public EditList toEditList() {
374 		final EditListtList.html#EditList">EditList r = new EditList();
375 		for (HunkHeader hunk : hunks)
376 			r.addAll(hunk.toEditList());
377 		return r;
378 	}
379 
380 	/**
381 	 * Parse a "diff --git" or "diff --cc" line.
382 	 *
383 	 * @param ptr
384 	 *            first character after the "diff --git " or "diff --cc " part.
385 	 * @param end
386 	 *            one past the last position to parse.
387 	 * @return first character after the LF at the end of the line; -1 on error.
388 	 */
389 	int parseGitFileName(int ptr, int end) {
390 		final int eol = nextLF(buf, ptr);
391 		final int bol = ptr;
392 		if (eol >= end) {
393 			return -1;
394 		}
395 
396 		// buffer[ptr..eol] looks like "a/foo b/foo\n". After the first
397 		// A regex to match this is "^[^/]+/(.*?) [^/+]+/\1\n$". There
398 		// is only one way to split the line such that text to the left
399 		// of the space matches the text to the right, excluding the part
400 		// before the first slash.
401 		//
402 
403 		final int aStart = nextLF(buf, ptr, '/');
404 		if (aStart >= eol)
405 			return eol;
406 
407 		while (ptr < eol) {
408 			final int sp = nextLF(buf, ptr, ' ');
409 			if (sp >= eol) {
410 				// We can't split the header, it isn't valid.
411 				// This may be OK if this is a rename patch.
412 				//
413 				return eol;
414 			}
415 			final int bStart = nextLF(buf, sp, '/');
416 			if (bStart >= eol)
417 				return eol;
418 
419 			// If buffer[aStart..sp - 1] = buffer[bStart..eol - 1]
420 			// we have a valid split.
421 			//
422 			if (eq(aStart, sp - 1, bStart, eol - 1)) {
423 				if (buf[bol] == '"') {
424 					// We're a double quoted name. The region better end
425 					// in a double quote too, and we need to decode the
426 					// characters before reading the name.
427 					//
428 					if (buf[sp - 2] != '"') {
429 						return eol;
430 					}
431 					oldPath = QuotedString.GIT_PATH.dequote(buf, bol, sp - 1);
432 					oldPath = p1(oldPath);
433 				} else {
434 					oldPath = decode(UTF_8, buf, aStart, sp - 1);
435 				}
436 				newPath = oldPath;
437 				return eol;
438 			}
439 
440 			// This split wasn't correct. Move past the space and try
441 			// another split as the space must be part of the file name.
442 			//
443 			ptr = sp;
444 		}
445 
446 		return eol;
447 	}
448 
449 	int parseGitHeaders(int ptr, int end) {
450 		while (ptr < end) {
451 			final int eol = nextLF(buf, ptr);
452 			if (isHunkHdr(buf, ptr, eol) >= 1) {
453 				// First hunk header; break out and parse them later.
454 				break;
455 
456 			} else if (match(buf, ptr, OLD_NAME) >= 0) {
457 				parseOldName(ptr, eol);
458 
459 			} else if (match(buf, ptr, NEW_NAME) >= 0) {
460 				parseNewName(ptr, eol);
461 
462 			} else if (match(buf, ptr, OLD_MODE) >= 0) {
463 				oldMode = parseFileMode(ptr + OLD_MODE.length, eol);
464 
465 			} else if (match(buf, ptr, NEW_MODE) >= 0) {
466 				newMode = parseFileMode(ptr + NEW_MODE.length, eol);
467 
468 			} else if (match(buf, ptr, DELETED_FILE_MODE) >= 0) {
469 				oldMode = parseFileMode(ptr + DELETED_FILE_MODE.length, eol);
470 				newMode = FileMode.MISSING;
471 				changeType = ChangeType.DELETE;
472 
473 			} else if (match(buf, ptr, NEW_FILE_MODE) >= 0) {
474 				parseNewFileMode(ptr, eol);
475 
476 			} else if (match(buf, ptr, COPY_FROM) >= 0) {
477 				oldPath = parseName(oldPath, ptr + COPY_FROM.length, eol);
478 				changeType = ChangeType.COPY;
479 
480 			} else if (match(buf, ptr, COPY_TO) >= 0) {
481 				newPath = parseName(newPath, ptr + COPY_TO.length, eol);
482 				changeType = ChangeType.COPY;
483 
484 			} else if (match(buf, ptr, RENAME_OLD) >= 0) {
485 				oldPath = parseName(oldPath, ptr + RENAME_OLD.length, eol);
486 				changeType = ChangeType.RENAME;
487 
488 			} else if (match(buf, ptr, RENAME_NEW) >= 0) {
489 				newPath = parseName(newPath, ptr + RENAME_NEW.length, eol);
490 				changeType = ChangeType.RENAME;
491 
492 			} else if (match(buf, ptr, RENAME_FROM) >= 0) {
493 				oldPath = parseName(oldPath, ptr + RENAME_FROM.length, eol);
494 				changeType = ChangeType.RENAME;
495 
496 			} else if (match(buf, ptr, RENAME_TO) >= 0) {
497 				newPath = parseName(newPath, ptr + RENAME_TO.length, eol);
498 				changeType = ChangeType.RENAME;
499 
500 			} else if (match(buf, ptr, SIMILARITY_INDEX) >= 0) {
501 				score = parseBase10(buf, ptr + SIMILARITY_INDEX.length, null);
502 
503 			} else if (match(buf, ptr, DISSIMILARITY_INDEX) >= 0) {
504 				score = parseBase10(buf, ptr + DISSIMILARITY_INDEX.length, null);
505 
506 			} else if (match(buf, ptr, INDEX) >= 0) {
507 				parseIndexLine(ptr + INDEX.length, eol);
508 
509 			} else {
510 				// Probably an empty patch (stat dirty).
511 				break;
512 			}
513 
514 			ptr = eol;
515 		}
516 		return ptr;
517 	}
518 
519 	void parseOldName(int ptr, int eol) {
520 		oldPath = p1(parseName(oldPath, ptr + OLD_NAME.length, eol));
521 		if (oldPath == DEV_NULL)
522 			changeType = ChangeType.ADD;
523 	}
524 
525 	void parseNewName(int ptr, int eol) {
526 		newPath = p1(parseName(newPath, ptr + NEW_NAME.length, eol));
527 		if (newPath == DEV_NULL)
528 			changeType = ChangeType.DELETE;
529 	}
530 
531 	void parseNewFileMode(int ptr, int eol) {
532 		oldMode = FileMode.MISSING;
533 		newMode = parseFileMode(ptr + NEW_FILE_MODE.length, eol);
534 		changeType = ChangeType.ADD;
535 	}
536 
537 	int parseTraditionalHeaders(int ptr, int end) {
538 		while (ptr < end) {
539 			final int eol = nextLF(buf, ptr);
540 			if (isHunkHdr(buf, ptr, eol) >= 1) {
541 				// First hunk header; break out and parse them later.
542 				break;
543 
544 			} else if (match(buf, ptr, OLD_NAME) >= 0) {
545 				parseOldName(ptr, eol);
546 
547 			} else if (match(buf, ptr, NEW_NAME) >= 0) {
548 				parseNewName(ptr, eol);
549 
550 			} else {
551 				// Possibly an empty patch.
552 				break;
553 			}
554 
555 			ptr = eol;
556 		}
557 		return ptr;
558 	}
559 
560 	private String parseName(String expect, int ptr, int end) {
561 		if (ptr == end)
562 			return expect;
563 
564 		String r;
565 		if (buf[ptr] == '"') {
566 			// New style GNU diff format
567 			//
568 			r = QuotedString.GIT_PATH.dequote(buf, ptr, end - 1);
569 		} else {
570 			// Older style GNU diff format, an optional tab ends the name.
571 			//
572 			int tab = end;
573 			while (ptr < tab && buf[tab - 1] != '\t')
574 				tab--;
575 			if (ptr == tab)
576 				tab = end;
577 			r = decode(UTF_8, buf, ptr, tab - 1);
578 		}
579 
580 		if (r.equals(DEV_NULL))
581 			r = DEV_NULL;
582 		return r;
583 	}
584 
585 	private static String p1(final String r) {
586 		final int s = r.indexOf('/');
587 		return s > 0 ? r.substring(s + 1) : r;
588 	}
589 
590 	FileMode parseFileMode(int ptr, int end) {
591 		int tmp = 0;
592 		while (ptr < end - 1) {
593 			tmp <<= 3;
594 			tmp += buf[ptr++] - '0';
595 		}
596 		return FileMode.fromBits(tmp);
597 	}
598 
599 	void parseIndexLine(int ptr, int end) {
600 		// "index $asha1..$bsha1[ $mode]" where $asha1 and $bsha1
601 		// can be unique abbreviations
602 		//
603 		final int dot2 = nextLF(buf, ptr, '.');
604 		final int mode = nextLF(buf, dot2, ' ');
605 
606 		oldId = AbbreviatedObjectId.fromString(buf, ptr, dot2 - 1);
607 		newId = AbbreviatedObjectId.fromString(buf, dot2 + 1, mode - 1);
608 
609 		if (mode < end)
610 			newMode = oldMode = parseFileMode(mode, end);
611 	}
612 
613 	private boolean eq(int aPtr, int aEnd, int bPtr, int bEnd) {
614 		if (aEnd - aPtr != bEnd - bPtr) {
615 			return false;
616 		}
617 		while (aPtr < aEnd) {
618 			if (buf[aPtr++] != buf[bPtr++])
619 				return false;
620 		}
621 		return true;
622 	}
623 
624 	/**
625 	 * Determine if this is a patch hunk header.
626 	 *
627 	 * @param buf
628 	 *            the buffer to scan
629 	 * @param start
630 	 *            first position in the buffer to evaluate
631 	 * @param end
632 	 *            last position to consider; usually the end of the buffer (
633 	 *            <code>buf.length</code>) or the first position on the next
634 	 *            line. This is only used to avoid very long runs of '@' from
635 	 *            killing the scan loop.
636 	 * @return the number of "ancestor revisions" in the hunk header. A
637 	 *         traditional two-way diff ("@@ -...") returns 1; a combined diff
638 	 *         for a 3 way-merge returns 3. If this is not a hunk header, 0 is
639 	 *         returned instead.
640 	 */
641 	static int isHunkHdr(byte[] buf, int start, int end) {
642 		int ptr = start;
643 		while (ptr < end && buf[ptr] == '@')
644 			ptr++;
645 		if (ptr - start < 2)
646 			return 0;
647 		if (ptr == end || buf[ptr++] != ' ')
648 			return 0;
649 		if (ptr == end || buf[ptr++] != '-')
650 			return 0;
651 		return (ptr - 3) - start;
652 	}
653 }