View Javadoc
1   /*
2    * Copyright (C) 2009, Google Inc.
3    * Copyright (C) 2008-2009, Johannes E. Schindelin <johannes.schindelin@gmx.de>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  
45  package org.eclipse.jgit.diff;
46  
47  import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
48  import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
49  import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
50  import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
51  import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
52  import static org.eclipse.jgit.diff.DiffEntry.Side.NEW;
53  import static org.eclipse.jgit.diff.DiffEntry.Side.OLD;
54  import static org.eclipse.jgit.lib.Constants.encode;
55  import static org.eclipse.jgit.lib.Constants.encodeASCII;
56  import static org.eclipse.jgit.lib.FileMode.GITLINK;
57  
58  import java.io.ByteArrayOutputStream;
59  import java.io.IOException;
60  import java.io.OutputStream;
61  import java.util.Collection;
62  import java.util.Collections;
63  import java.util.List;
64  
65  import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
66  import org.eclipse.jgit.diff.DiffEntry.ChangeType;
67  import org.eclipse.jgit.dircache.DirCacheIterator;
68  import org.eclipse.jgit.errors.AmbiguousObjectException;
69  import org.eclipse.jgit.errors.CorruptObjectException;
70  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
71  import org.eclipse.jgit.errors.LargeObjectException;
72  import org.eclipse.jgit.errors.MissingObjectException;
73  import org.eclipse.jgit.internal.JGitText;
74  import org.eclipse.jgit.lib.AbbreviatedObjectId;
75  import org.eclipse.jgit.lib.AnyObjectId;
76  import org.eclipse.jgit.lib.Config;
77  import org.eclipse.jgit.lib.ConfigConstants;
78  import org.eclipse.jgit.lib.Constants;
79  import org.eclipse.jgit.lib.FileMode;
80  import org.eclipse.jgit.lib.ObjectId;
81  import org.eclipse.jgit.lib.ObjectLoader;
82  import org.eclipse.jgit.lib.ObjectReader;
83  import org.eclipse.jgit.lib.ProgressMonitor;
84  import org.eclipse.jgit.lib.Repository;
85  import org.eclipse.jgit.patch.FileHeader;
86  import org.eclipse.jgit.patch.FileHeader.PatchType;
87  import org.eclipse.jgit.patch.HunkHeader;
88  import org.eclipse.jgit.revwalk.FollowFilter;
89  import org.eclipse.jgit.revwalk.RevTree;
90  import org.eclipse.jgit.revwalk.RevWalk;
91  import org.eclipse.jgit.storage.pack.PackConfig;
92  import org.eclipse.jgit.treewalk.AbstractTreeIterator;
93  import org.eclipse.jgit.treewalk.CanonicalTreeParser;
94  import org.eclipse.jgit.treewalk.EmptyTreeIterator;
95  import org.eclipse.jgit.treewalk.TreeWalk;
96  import org.eclipse.jgit.treewalk.WorkingTreeIterator;
97  import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
98  import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
99  import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
100 import org.eclipse.jgit.treewalk.filter.PathFilter;
101 import org.eclipse.jgit.treewalk.filter.TreeFilter;
102 import org.eclipse.jgit.util.QuotedString;
103 import org.eclipse.jgit.util.io.DisabledOutputStream;
104 
105 /**
106  * Format a Git style patch script.
107  */
108 public class DiffFormatter implements AutoCloseable {
109 	private static final int DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
110 
111 	private static final byte[] noNewLine = encodeASCII("\\ No newline at end of file\n"); //$NON-NLS-1$
112 
113 	/** Magic return content indicating it is empty or no content present. */
114 	private static final byte[] EMPTY = new byte[] {};
115 
116 	/** Magic return indicating the content is binary. */
117 	private static final byte[] BINARY = new byte[] {};
118 
119 	private final OutputStream out;
120 
121 	private ObjectReader reader;
122 
123 	private boolean closeReader;
124 
125 	private DiffConfig diffCfg;
126 
127 	private int context = 3;
128 
129 	private int abbreviationLength = 7;
130 
131 	private DiffAlgorithm diffAlgorithm;
132 
133 	private RawTextComparator comparator = RawTextComparator.DEFAULT;
134 
135 	private int binaryFileThreshold = DEFAULT_BINARY_FILE_THRESHOLD;
136 
137 	private String oldPrefix = "a/"; //$NON-NLS-1$
138 
139 	private String newPrefix = "b/"; //$NON-NLS-1$
140 
141 	private TreeFilter pathFilter = TreeFilter.ALL;
142 
143 	private RenameDetector renameDetector;
144 
145 	private ProgressMonitor progressMonitor;
146 
147 	private ContentSource.Pair source;
148 
149 	/**
150 	 * Create a new formatter with a default level of context.
151 	 *
152 	 * @param out
153 	 *            the stream the formatter will write line data to. This stream
154 	 *            should have buffering arranged by the caller, as many small
155 	 *            writes are performed to it.
156 	 */
157 	public DiffFormatter(OutputStream out) {
158 		this.out = out;
159 	}
160 
161 	/** @return the stream we are outputting data to. */
162 	protected OutputStream getOutputStream() {
163 		return out;
164 	}
165 
166 	/**
167 	 * Set the repository the formatter can load object contents from.
168 	 *
169 	 * Once a repository has been set, the formatter must be released to ensure
170 	 * the internal ObjectReader is able to release its resources.
171 	 *
172 	 * @param repository
173 	 *            source repository holding referenced objects.
174 	 */
175 	public void setRepository(Repository repository) {
176 		setReader(repository.newObjectReader(), repository.getConfig(), true);
177 	}
178 
179 	/**
180 	 * Set the repository the formatter can load object contents from.
181 	 *
182 	 * @param reader
183 	 *            source reader holding referenced objects. Caller is responsible
184 	 *            for closing the reader.
185 	 * @param cfg
186 	 *            config specifying diff algorithm and rename detection options.
187 	 * @since 4.5
188 	 */
189 	public void setReader(ObjectReader reader, Config cfg) {
190 		setReader(reader, cfg, false);
191 	}
192 
193 	private void setReader(ObjectReader reader, Config cfg, boolean closeReader) {
194 		close();
195 		this.closeReader = closeReader;
196 		this.reader = reader;
197 		this.diffCfg = cfg.get(DiffConfig.KEY);
198 
199 		ContentSource cs = ContentSource.create(reader);
200 		source = new ContentSource.Pair(cs, cs);
201 
202 		if (diffCfg.isNoPrefix()) {
203 			setOldPrefix(""); //$NON-NLS-1$
204 			setNewPrefix(""); //$NON-NLS-1$
205 		}
206 		setDetectRenames(diffCfg.isRenameDetectionEnabled());
207 
208 		diffAlgorithm = DiffAlgorithm.getAlgorithm(cfg.getEnum(
209 				ConfigConstants.CONFIG_DIFF_SECTION, null,
210 				ConfigConstants.CONFIG_KEY_ALGORITHM,
211 				SupportedAlgorithm.HISTOGRAM));
212 	}
213 
214 	/**
215 	 * Change the number of lines of context to display.
216 	 *
217 	 * @param lineCount
218 	 *            number of lines of context to see before the first
219 	 *            modification and after the last modification within a hunk of
220 	 *            the modified file.
221 	 */
222 	public void setContext(final int lineCount) {
223 		if (lineCount < 0)
224 			throw new IllegalArgumentException(
225 					JGitText.get().contextMustBeNonNegative);
226 		context = lineCount;
227 	}
228 
229 	/**
230 	 * Change the number of digits to show in an ObjectId.
231 	 *
232 	 * @param count
233 	 *            number of digits to show in an ObjectId.
234 	 */
235 	public void setAbbreviationLength(final int count) {
236 		if (count < 0)
237 			throw new IllegalArgumentException(
238 					JGitText.get().abbreviationLengthMustBeNonNegative);
239 		abbreviationLength = count;
240 	}
241 
242 	/**
243 	 * Set the algorithm that constructs difference output.
244 	 *
245 	 * @param alg
246 	 *            the algorithm to produce text file differences.
247 	 * @see HistogramDiff
248 	 */
249 	public void setDiffAlgorithm(DiffAlgorithm alg) {
250 		diffAlgorithm = alg;
251 	}
252 
253 	/**
254 	 * Set the line equivalence function for text file differences.
255 	 *
256 	 * @param cmp
257 	 *            The equivalence function used to determine if two lines of
258 	 *            text are identical. The function can be changed to ignore
259 	 *            various types of whitespace.
260 	 * @see RawTextComparator#DEFAULT
261 	 * @see RawTextComparator#WS_IGNORE_ALL
262 	 * @see RawTextComparator#WS_IGNORE_CHANGE
263 	 * @see RawTextComparator#WS_IGNORE_LEADING
264 	 * @see RawTextComparator#WS_IGNORE_TRAILING
265 	 */
266 	public void setDiffComparator(RawTextComparator cmp) {
267 		comparator = cmp;
268 	}
269 
270 	/**
271 	 * Set maximum file size for text files.
272 	 *
273 	 * Files larger than this size will be treated as though they are binary and
274 	 * not text. Default is {@value #DEFAULT_BINARY_FILE_THRESHOLD} .
275 	 *
276 	 * @param threshold
277 	 *            the limit, in bytes. Files larger than this size will be
278 	 *            assumed to be binary, even if they aren't.
279 	 */
280 	public void setBinaryFileThreshold(int threshold) {
281 		this.binaryFileThreshold = threshold;
282 	}
283 
284 	/**
285 	 * Set the prefix applied in front of old file paths.
286 	 *
287 	 * @param prefix
288 	 *            the prefix in front of old paths. Typically this is the
289 	 *            standard string {@code "a/"}, but may be any prefix desired by
290 	 *            the caller. Must not be null. Use the empty string to have no
291 	 *            prefix at all.
292 	 */
293 	public void setOldPrefix(String prefix) {
294 		oldPrefix = prefix;
295 	}
296 
297 	/**
298 	 * Get the prefix applied in front of old file paths.
299 	 *
300 	 * @return the prefix
301 	 * @since 2.0
302 	 */
303 	public String getOldPrefix() {
304 		return this.oldPrefix;
305 	}
306 
307 	/**
308 	 * Set the prefix applied in front of new file paths.
309 	 *
310 	 * @param prefix
311 	 *            the prefix in front of new paths. Typically this is the
312 	 *            standard string {@code "b/"}, but may be any prefix desired by
313 	 *            the caller. Must not be null. Use the empty string to have no
314 	 *            prefix at all.
315 	 */
316 	public void setNewPrefix(String prefix) {
317 		newPrefix = prefix;
318 	}
319 
320 	/**
321 	 * Get the prefix applied in front of new file paths.
322 	 *
323 	 * @return the prefix
324 	 * @since 2.0
325 	 */
326 	public String getNewPrefix() {
327 		return this.newPrefix;
328 	}
329 
330 	/** @return true if rename detection is enabled. */
331 	public boolean isDetectRenames() {
332 		return renameDetector != null;
333 	}
334 
335 	/**
336 	 * Enable or disable rename detection.
337 	 *
338 	 * Before enabling rename detection the repository must be set with
339 	 * {@link #setRepository(Repository)}. Once enabled the detector can be
340 	 * configured away from its defaults by obtaining the instance directly from
341 	 * {@link #getRenameDetector()} and invoking configuration.
342 	 *
343 	 * @param on
344 	 *            if rename detection should be enabled.
345 	 */
346 	public void setDetectRenames(boolean on) {
347 		if (on && renameDetector == null) {
348 			assertHaveReader();
349 			renameDetector = new RenameDetector(reader, diffCfg);
350 		} else if (!on)
351 			renameDetector = null;
352 	}
353 
354 	/** @return the rename detector if rename detection is enabled. */
355 	public RenameDetector getRenameDetector() {
356 		return renameDetector;
357 	}
358 
359 	/**
360 	 * Set the progress monitor for long running rename detection.
361 	 *
362 	 * @param pm
363 	 *            progress monitor to receive rename detection status through.
364 	 */
365 	public void setProgressMonitor(ProgressMonitor pm) {
366 		progressMonitor = pm;
367 	}
368 
369 	/**
370 	 * Set the filter to produce only specific paths.
371 	 *
372 	 * If the filter is an instance of {@link FollowFilter}, the filter path
373 	 * will be updated during successive scan or format invocations. The updated
374 	 * path can be obtained from {@link #getPathFilter()}.
375 	 *
376 	 * @param filter
377 	 *            the tree filter to apply.
378 	 */
379 	public void setPathFilter(TreeFilter filter) {
380 		pathFilter = filter != null ? filter : TreeFilter.ALL;
381 	}
382 
383 	/** @return the current path filter. */
384 	public TreeFilter getPathFilter() {
385 		return pathFilter;
386 	}
387 
388 	/**
389 	 * Flush the underlying output stream of this formatter.
390 	 *
391 	 * @throws IOException
392 	 *             the stream's own flush method threw an exception.
393 	 */
394 	public void flush() throws IOException {
395 		out.flush();
396 	}
397 
398 	/**
399 	 * Release the internal ObjectReader state.
400 	 *
401 	 * @since 4.0
402 	 */
403 	@Override
404 	public void close() {
405 		if (reader != null && closeReader) {
406 			reader.close();
407 		}
408 	}
409 
410 	/**
411 	 * Determine the differences between two trees.
412 	 *
413 	 * No output is created, instead only the file paths that are different are
414 	 * returned. Callers may choose to format these paths themselves, or convert
415 	 * them into {@link FileHeader} instances with a complete edit list by
416 	 * calling {@link #toFileHeader(DiffEntry)}.
417 	 * <p>
418 	 * Either side may be null to indicate that the tree has beed added or
419 	 * removed. The diff will be computed against nothing.
420 	 *
421 	 * @param a
422 	 *            the old (or previous) side or null
423 	 * @param b
424 	 *            the new (or updated) side or null
425 	 * @return the paths that are different.
426 	 * @throws IOException
427 	 *             trees cannot be read or file contents cannot be read.
428 	 */
429 	public List<DiffEntry> scan(AnyObjectId a, AnyObjectId b)
430 			throws IOException {
431 		assertHaveReader();
432 
433 		try (RevWalk rw = new RevWalk(reader)) {
434 			RevTree aTree = a != null ? rw.parseTree(a) : null;
435 			RevTree bTree = b != null ? rw.parseTree(b) : null;
436 			return scan(aTree, bTree);
437 		}
438 	}
439 
440 	/**
441 	 * Determine the differences between two trees.
442 	 *
443 	 * No output is created, instead only the file paths that are different are
444 	 * returned. Callers may choose to format these paths themselves, or convert
445 	 * them into {@link FileHeader} instances with a complete edit list by
446 	 * calling {@link #toFileHeader(DiffEntry)}.
447 	 * <p>
448 	 * Either side may be null to indicate that the tree has beed added or
449 	 * removed. The diff will be computed against nothing.
450 	 *
451 	 * @param a
452 	 *            the old (or previous) side or null
453 	 * @param b
454 	 *            the new (or updated) side or null
455 	 * @return the paths that are different.
456 	 * @throws IOException
457 	 *             trees cannot be read or file contents cannot be read.
458 	 */
459 	public List<DiffEntry> scan(RevTree a, RevTree b) throws IOException {
460 		assertHaveReader();
461 
462 		AbstractTreeIterator aIterator = makeIteratorFromTreeOrNull(a);
463 		AbstractTreeIterator bIterator = makeIteratorFromTreeOrNull(b);
464 		return scan(aIterator, bIterator);
465 	}
466 
467 	private AbstractTreeIterator makeIteratorFromTreeOrNull(RevTree tree)
468 			throws IncorrectObjectTypeException, IOException {
469 		if (tree != null) {
470 			CanonicalTreeParser parser = new CanonicalTreeParser();
471 			parser.reset(reader, tree);
472 			return parser;
473 		} else
474 			return new EmptyTreeIterator();
475 	}
476 
477 	/**
478 	 * Determine the differences between two trees.
479 	 *
480 	 * No output is created, instead only the file paths that are different are
481 	 * returned. Callers may choose to format these paths themselves, or convert
482 	 * them into {@link FileHeader} instances with a complete edit list by
483 	 * calling {@link #toFileHeader(DiffEntry)}.
484 	 *
485 	 * @param a
486 	 *            the old (or previous) side.
487 	 * @param b
488 	 *            the new (or updated) side.
489 	 * @return the paths that are different.
490 	 * @throws IOException
491 	 *             trees cannot be read or file contents cannot be read.
492 	 */
493 	public List<DiffEntry> scan(AbstractTreeIterator a, AbstractTreeIterator b)
494 			throws IOException {
495 		assertHaveReader();
496 
497 		TreeWalk walk = new TreeWalk(reader);
498 		walk.addTree(a);
499 		walk.addTree(b);
500 		walk.setRecursive(true);
501 
502 		TreeFilter filter = getDiffTreeFilterFor(a, b);
503 		if (pathFilter instanceof FollowFilter) {
504 			walk.setFilter(AndTreeFilter.create(
505 					PathFilter.create(((FollowFilter) pathFilter).getPath()),
506 					filter));
507 		} else {
508 			walk.setFilter(AndTreeFilter.create(pathFilter, filter));
509 		}
510 
511 		source = new ContentSource.Pair(source(a), source(b));
512 
513 		List<DiffEntry> files = DiffEntry.scan(walk);
514 		if (pathFilter instanceof FollowFilter && isAdd(files)) {
515 			// The file we are following was added here, find where it
516 			// came from so we can properly show the rename or copy,
517 			// then continue digging backwards.
518 			//
519 			a.reset();
520 			b.reset();
521 			walk.reset();
522 			walk.addTree(a);
523 			walk.addTree(b);
524 			walk.setFilter(filter);
525 
526 			if (renameDetector == null)
527 				setDetectRenames(true);
528 			files = updateFollowFilter(detectRenames(DiffEntry.scan(walk)));
529 
530 		} else if (renameDetector != null)
531 			files = detectRenames(files);
532 
533 		return files;
534 	}
535 
536 	private static TreeFilter getDiffTreeFilterFor(AbstractTreeIterator a,
537 			AbstractTreeIterator b) {
538 		if (a instanceof DirCacheIterator && b instanceof WorkingTreeIterator)
539 			return new IndexDiffFilter(0, 1);
540 
541 		if (a instanceof WorkingTreeIterator && b instanceof DirCacheIterator)
542 			return new IndexDiffFilter(1, 0);
543 
544 		TreeFilter filter = TreeFilter.ANY_DIFF;
545 		if (a instanceof WorkingTreeIterator)
546 			filter = AndTreeFilter.create(new NotIgnoredFilter(0), filter);
547 		if (b instanceof WorkingTreeIterator)
548 			filter = AndTreeFilter.create(new NotIgnoredFilter(1), filter);
549 		return filter;
550 	}
551 
552 	private ContentSource source(AbstractTreeIterator iterator) {
553 		if (iterator instanceof WorkingTreeIterator)
554 			return ContentSource.create((WorkingTreeIterator) iterator);
555 		return ContentSource.create(reader);
556 	}
557 
558 	private List<DiffEntry> detectRenames(List<DiffEntry> files)
559 			throws IOException {
560 		renameDetector.reset();
561 		renameDetector.addAll(files);
562 		return renameDetector.compute(reader, progressMonitor);
563 	}
564 
565 	private boolean isAdd(List<DiffEntry> files) {
566 		String oldPath = ((FollowFilter) pathFilter).getPath();
567 		for (DiffEntry ent : files) {
568 			if (ent.getChangeType() == ADD && ent.getNewPath().equals(oldPath))
569 				return true;
570 		}
571 		return false;
572 	}
573 
574 	private List<DiffEntry> updateFollowFilter(List<DiffEntry> files) {
575 		String oldPath = ((FollowFilter) pathFilter).getPath();
576 		for (DiffEntry ent : files) {
577 			if (isRename(ent) && ent.getNewPath().equals(oldPath)) {
578 				pathFilter = FollowFilter.create(ent.getOldPath(), diffCfg);
579 				return Collections.singletonList(ent);
580 			}
581 		}
582 		return Collections.emptyList();
583 	}
584 
585 	private static boolean isRename(DiffEntry ent) {
586 		return ent.getChangeType() == RENAME || ent.getChangeType() == COPY;
587 	}
588 
589 	/**
590 	 * Format the differences between two trees.
591 	 *
592 	 * The patch is expressed as instructions to modify {@code a} to make it
593 	 * {@code b}.
594 	 * <p>
595 	 * Either side may be null to indicate that the tree has beed added or
596 	 * removed. The diff will be computed against nothing.
597 	 *
598 	 * @param a
599 	 *            the old (or previous) side or null
600 	 * @param b
601 	 *            the new (or updated) side or null
602 	 * @throws IOException
603 	 *             trees cannot be read, file contents cannot be read, or the
604 	 *             patch cannot be output.
605 	 */
606 	public void format(AnyObjectId a, AnyObjectId b) throws IOException {
607 		format(scan(a, b));
608 	}
609 
610 	/**
611 	 * Format the differences between two trees.
612 	 *
613 	 * The patch is expressed as instructions to modify {@code a} to make it
614 	 * {@code b}.
615 	 *
616 	 * <p>
617 	 * Either side may be null to indicate that the tree has beed added or
618 	 * removed. The diff will be computed against nothing.
619 	 *
620 	 * @param a
621 	 *            the old (or previous) side or null
622 	 * @param b
623 	 *            the new (or updated) side or null
624 	 * @throws IOException
625 	 *             trees cannot be read, file contents cannot be read, or the
626 	 *             patch cannot be output.
627 	 */
628 	public void format(RevTree a, RevTree b) throws IOException {
629 		format(scan(a, b));
630 	}
631 
632 	/**
633 	 * Format the differences between two trees.
634 	 *
635 	 * The patch is expressed as instructions to modify {@code a} to make it
636 	 * {@code b}.
637 	 * <p>
638 	 * Either side may be null to indicate that the tree has beed added or
639 	 * removed. The diff will be computed against nothing.
640 	 *
641 	 * @param a
642 	 *            the old (or previous) side or null
643 	 * @param b
644 	 *            the new (or updated) side or null
645 	 * @throws IOException
646 	 *             trees cannot be read, file contents cannot be read, or the
647 	 *             patch cannot be output.
648 	 */
649 	public void format(AbstractTreeIterator a, AbstractTreeIterator b)
650 			throws IOException {
651 		format(scan(a, b));
652 	}
653 
654 	/**
655 	 * Format a patch script from a list of difference entries. Requires
656 	 * {@link #scan(AbstractTreeIterator, AbstractTreeIterator)} to have been
657 	 * called first.
658 	 *
659 	 * @param entries
660 	 *            entries describing the affected files.
661 	 * @throws IOException
662 	 *             a file's content cannot be read, or the output stream cannot
663 	 *             be written to.
664 	 */
665 	public void format(List<? extends DiffEntry> entries) throws IOException {
666 		for (DiffEntry ent : entries)
667 			format(ent);
668 	}
669 
670 	/**
671 	 * Format a patch script for one file entry.
672 	 *
673 	 * @param ent
674 	 *            the entry to be formatted.
675 	 * @throws IOException
676 	 *             a file's content cannot be read, or the output stream cannot
677 	 *             be written to.
678 	 */
679 	public void format(DiffEntry ent) throws IOException {
680 		FormatResult res = createFormatResult(ent);
681 		format(res.header, res.a, res.b);
682 	}
683 
684 	private static byte[] writeGitLinkText(AbbreviatedObjectId id) {
685 		if (ObjectId.zeroId().equals(id.toObjectId())) {
686 			return EMPTY;
687 		}
688 		return encodeASCII("Subproject commit " + id.name() //$NON-NLS-1$
689 				+ "\n"); //$NON-NLS-1$
690 	}
691 
692 	private String format(AbbreviatedObjectId id) {
693 		if (id.isComplete() && reader != null) {
694 			try {
695 				id = reader.abbreviate(id.toObjectId(), abbreviationLength);
696 			} catch (IOException cannotAbbreviate) {
697 				// Ignore this. We'll report the full identity.
698 			}
699 		}
700 		return id.name();
701 	}
702 
703 	private static String quotePath(String name) {
704 		return QuotedString.GIT_PATH.quote(name);
705 	}
706 
707 	/**
708 	 * Format a patch script, reusing a previously parsed FileHeader.
709 	 * <p>
710 	 * This formatter is primarily useful for editing an existing patch script
711 	 * to increase or reduce the number of lines of context within the script.
712 	 * All header lines are reused as-is from the supplied FileHeader.
713 	 *
714 	 * @param head
715 	 *            existing file header containing the header lines to copy.
716 	 * @param a
717 	 *            text source for the pre-image version of the content. This
718 	 *            must match the content of {@link FileHeader#getOldId()}.
719 	 * @param b
720 	 *            text source for the post-image version of the content. This
721 	 *            must match the content of {@link FileHeader#getNewId()}.
722 	 * @throws IOException
723 	 *             writing to the supplied stream failed.
724 	 */
725 	public void format(final FileHeader head, final RawText a, final RawText b)
726 			throws IOException {
727 		// Reuse the existing FileHeader as-is by blindly copying its
728 		// header lines, but avoiding its hunks. Instead we recreate
729 		// the hunks from the text instances we have been supplied.
730 		//
731 		final int start = head.getStartOffset();
732 		int end = head.getEndOffset();
733 		if (!head.getHunks().isEmpty())
734 			end = head.getHunks().get(0).getStartOffset();
735 		out.write(head.getBuffer(), start, end - start);
736 		if (head.getPatchType() == PatchType.UNIFIED)
737 			format(head.toEditList(), a, b);
738 	}
739 
740 	/**
741 	 * Formats a list of edits in unified diff format
742 	 *
743 	 * @param edits
744 	 *            some differences which have been calculated between A and B
745 	 * @param a
746 	 *            the text A which was compared
747 	 * @param b
748 	 *            the text B which was compared
749 	 * @throws IOException
750 	 */
751 	public void format(final EditList edits, final RawText a, final RawText b)
752 			throws IOException {
753 		for (int curIdx = 0; curIdx < edits.size();) {
754 			Edit curEdit = edits.get(curIdx);
755 			final int endIdx = findCombinedEnd(edits, curIdx);
756 			final Edit endEdit = edits.get(endIdx);
757 
758 			int aCur = (int) Math.max(0, (long) curEdit.getBeginA() - context);
759 			int bCur = (int) Math.max(0, (long) curEdit.getBeginB() - context);
760 			final int aEnd = (int) Math.min(a.size(), (long) endEdit.getEndA() + context);
761 			final int bEnd = (int) Math.min(b.size(), (long) endEdit.getEndB() + context);
762 
763 			writeHunkHeader(aCur, aEnd, bCur, bEnd);
764 
765 			while (aCur < aEnd || bCur < bEnd) {
766 				if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) {
767 					writeContextLine(a, aCur);
768 					if (isEndOfLineMissing(a, aCur))
769 						out.write(noNewLine);
770 					aCur++;
771 					bCur++;
772 				} else if (aCur < curEdit.getEndA()) {
773 					writeRemovedLine(a, aCur);
774 					if (isEndOfLineMissing(a, aCur))
775 						out.write(noNewLine);
776 					aCur++;
777 				} else if (bCur < curEdit.getEndB()) {
778 					writeAddedLine(b, bCur);
779 					if (isEndOfLineMissing(b, bCur))
780 						out.write(noNewLine);
781 					bCur++;
782 				}
783 
784 				if (end(curEdit, aCur, bCur) && ++curIdx < edits.size())
785 					curEdit = edits.get(curIdx);
786 			}
787 		}
788 	}
789 
790 	/**
791 	 * Output a line of context (unmodified line).
792 	 *
793 	 * @param text
794 	 *            RawText for accessing raw data
795 	 * @param line
796 	 *            the line number within text
797 	 * @throws IOException
798 	 */
799 	protected void writeContextLine(final RawText text, final int line)
800 			throws IOException {
801 		writeLine(' ', text, line);
802 	}
803 
804 	private static boolean isEndOfLineMissing(final RawText text, final int line) {
805 		return line + 1 == text.size() && text.isMissingNewlineAtEnd();
806 	}
807 
808 	/**
809 	 * Output an added line.
810 	 *
811 	 * @param text
812 	 *            RawText for accessing raw data
813 	 * @param line
814 	 *            the line number within text
815 	 * @throws IOException
816 	 */
817 	protected void writeAddedLine(final RawText text, final int line)
818 			throws IOException {
819 		writeLine('+', text, line);
820 	}
821 
822 	/**
823 	 * Output a removed line
824 	 *
825 	 * @param text
826 	 *            RawText for accessing raw data
827 	 * @param line
828 	 *            the line number within text
829 	 * @throws IOException
830 	 */
831 	protected void writeRemovedLine(final RawText text, final int line)
832 			throws IOException {
833 		writeLine('-', text, line);
834 	}
835 
836 	/**
837 	 * Output a hunk header
838 	 *
839 	 * @param aStartLine
840 	 *            within first source
841 	 * @param aEndLine
842 	 *            within first source
843 	 * @param bStartLine
844 	 *            within second source
845 	 * @param bEndLine
846 	 *            within second source
847 	 * @throws IOException
848 	 */
849 	protected void writeHunkHeader(int aStartLine, int aEndLine,
850 			int bStartLine, int bEndLine) throws IOException {
851 		out.write('@');
852 		out.write('@');
853 		writeRange('-', aStartLine + 1, aEndLine - aStartLine);
854 		writeRange('+', bStartLine + 1, bEndLine - bStartLine);
855 		out.write(' ');
856 		out.write('@');
857 		out.write('@');
858 		out.write('\n');
859 	}
860 
861 	private void writeRange(final char prefix, final int begin, final int cnt)
862 			throws IOException {
863 		out.write(' ');
864 		out.write(prefix);
865 		switch (cnt) {
866 		case 0:
867 			// If the range is empty, its beginning number must be the
868 			// line just before the range, or 0 if the range is at the
869 			// start of the file stream. Here, begin is always 1 based,
870 			// so an empty file would produce "0,0".
871 			//
872 			out.write(encodeASCII(begin - 1));
873 			out.write(',');
874 			out.write('0');
875 			break;
876 
877 		case 1:
878 			// If the range is exactly one line, produce only the number.
879 			//
880 			out.write(encodeASCII(begin));
881 			break;
882 
883 		default:
884 			out.write(encodeASCII(begin));
885 			out.write(',');
886 			out.write(encodeASCII(cnt));
887 			break;
888 		}
889 	}
890 
891 	/**
892 	 * Write a standard patch script line.
893 	 *
894 	 * @param prefix
895 	 *            prefix before the line, typically '-', '+', ' '.
896 	 * @param text
897 	 *            the text object to obtain the line from.
898 	 * @param cur
899 	 *            line number to output.
900 	 * @throws IOException
901 	 *             the stream threw an exception while writing to it.
902 	 */
903 	protected void writeLine(final char prefix, final RawText text,
904 			final int cur) throws IOException {
905 		out.write(prefix);
906 		text.writeLine(out, cur);
907 		out.write('\n');
908 	}
909 
910 	/**
911 	 * Creates a {@link FileHeader} representing the given {@link DiffEntry}
912 	 * <p>
913 	 * This method does not use the OutputStream associated with this
914 	 * DiffFormatter instance. It is therefore safe to instantiate this
915 	 * DiffFormatter instance with a {@link DisabledOutputStream} if this method
916 	 * is the only one that will be used.
917 	 *
918 	 * @param ent
919 	 *            the DiffEntry to create the FileHeader for
920 	 * @return a FileHeader representing the DiffEntry. The FileHeader's buffer
921 	 *         will contain only the header of the diff output. It will also
922 	 *         contain one {@link HunkHeader}.
923 	 * @throws IOException
924 	 *             the stream threw an exception while writing to it, or one of
925 	 *             the blobs referenced by the DiffEntry could not be read.
926 	 * @throws CorruptObjectException
927 	 *             one of the blobs referenced by the DiffEntry is corrupt.
928 	 * @throws MissingObjectException
929 	 *             one of the blobs referenced by the DiffEntry is missing.
930 	 */
931 	public FileHeader toFileHeader(DiffEntry ent) throws IOException,
932 			CorruptObjectException, MissingObjectException {
933 		return createFormatResult(ent).header;
934 	}
935 
936 	private static class FormatResult {
937 		FileHeader header;
938 
939 		RawText a;
940 
941 		RawText b;
942 	}
943 
944 	private FormatResult createFormatResult(DiffEntry ent) throws IOException,
945 			CorruptObjectException, MissingObjectException {
946 		final FormatResult res = new FormatResult();
947 		ByteArrayOutputStream buf = new ByteArrayOutputStream();
948 		final EditList editList;
949 		final FileHeader.PatchType type;
950 
951 		formatHeader(buf, ent);
952 
953 		if (ent.getOldId() == null || ent.getNewId() == null) {
954 			// Content not changed (e.g. only mode, pure rename)
955 			editList = new EditList();
956 			type = PatchType.UNIFIED;
957 
958 		} else {
959 			assertHaveReader();
960 
961 			byte[] aRaw, bRaw;
962 
963 			if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
964 				aRaw = writeGitLinkText(ent.getOldId());
965 				bRaw = writeGitLinkText(ent.getNewId());
966 			} else {
967 				aRaw = open(OLD, ent);
968 				bRaw = open(NEW, ent);
969 			}
970 
971 			if (aRaw == BINARY || bRaw == BINARY //
972 					|| RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
973 				formatOldNewPaths(buf, ent);
974 				buf.write(encodeASCII("Binary files differ\n")); //$NON-NLS-1$
975 				editList = new EditList();
976 				type = PatchType.BINARY;
977 
978 			} else {
979 				res.a = new RawText(aRaw);
980 				res.b = new RawText(bRaw);
981 				editList = diff(res.a, res.b);
982 				type = PatchType.UNIFIED;
983 
984 				switch (ent.getChangeType()) {
985 				case RENAME:
986 				case COPY:
987 					if (!editList.isEmpty())
988 						formatOldNewPaths(buf, ent);
989 					break;
990 
991 				default:
992 					formatOldNewPaths(buf, ent);
993 					break;
994 				}
995 			}
996 		}
997 
998 		res.header = new FileHeader(buf.toByteArray(), editList, type);
999 		return res;
1000 	}
1001 
1002 	private EditList diff(RawText a, RawText b) {
1003 		return diffAlgorithm.diff(comparator, a, b);
1004 	}
1005 
1006 	private void assertHaveReader() {
1007 		if (reader == null) {
1008 			throw new IllegalStateException(JGitText.get().readerIsRequired);
1009 		}
1010 	}
1011 
1012 	private byte[] open(DiffEntry.Side side, DiffEntry entry)
1013 			throws IOException {
1014 		if (entry.getMode(side) == FileMode.MISSING)
1015 			return EMPTY;
1016 
1017 		if (entry.getMode(side).getObjectType() != Constants.OBJ_BLOB)
1018 			return EMPTY;
1019 
1020 		AbbreviatedObjectId id = entry.getId(side);
1021 		if (!id.isComplete()) {
1022 			Collection<ObjectId> ids = reader.resolve(id);
1023 			if (ids.size() == 1) {
1024 				id = AbbreviatedObjectId.fromObjectId(ids.iterator().next());
1025 				switch (side) {
1026 				case OLD:
1027 					entry.oldId = id;
1028 					break;
1029 				case NEW:
1030 					entry.newId = id;
1031 					break;
1032 				}
1033 			} else if (ids.size() == 0)
1034 				throw new MissingObjectException(id, Constants.OBJ_BLOB);
1035 			else
1036 				throw new AmbiguousObjectException(id, ids);
1037 		}
1038 
1039 		try {
1040 			ObjectLoader ldr = source.open(side, entry);
1041 			return ldr.getBytes(binaryFileThreshold);
1042 
1043 		} catch (LargeObjectException.ExceedsLimit overLimit) {
1044 			return BINARY;
1045 
1046 		} catch (LargeObjectException.ExceedsByteArrayLimit overLimit) {
1047 			return BINARY;
1048 
1049 		} catch (LargeObjectException.OutOfMemory tooBig) {
1050 			return BINARY;
1051 
1052 		} catch (LargeObjectException tooBig) {
1053 			tooBig.setObjectId(id.toObjectId());
1054 			throw tooBig;
1055 		}
1056 	}
1057 
1058 	/**
1059 	 * Output the first header line
1060 	 *
1061 	 * @param o
1062 	 *            The stream the formatter will write the first header line to
1063 	 * @param type
1064 	 *            The {@link ChangeType}
1065 	 * @param oldPath
1066 	 *            old path to the file
1067 	 * @param newPath
1068 	 *            new path to the file
1069 	 * @throws IOException
1070 	 *             the stream threw an exception while writing to it.
1071 	 */
1072 	protected void formatGitDiffFirstHeaderLine(ByteArrayOutputStream o,
1073 			final ChangeType type, final String oldPath, final String newPath)
1074 			throws IOException {
1075 		o.write(encodeASCII("diff --git ")); //$NON-NLS-1$
1076 		o.write(encode(quotePath(oldPrefix + (type == ADD ? newPath : oldPath))));
1077 		o.write(' ');
1078 		o.write(encode(quotePath(newPrefix
1079 				+ (type == DELETE ? oldPath : newPath))));
1080 		o.write('\n');
1081 	}
1082 
1083 	private void formatHeader(ByteArrayOutputStream o, DiffEntry ent)
1084 			throws IOException {
1085 		final ChangeType type = ent.getChangeType();
1086 		final String oldp = ent.getOldPath();
1087 		final String newp = ent.getNewPath();
1088 		final FileMode oldMode = ent.getOldMode();
1089 		final FileMode newMode = ent.getNewMode();
1090 
1091 		formatGitDiffFirstHeaderLine(o, type, oldp, newp);
1092 
1093 		if ((type == MODIFY || type == COPY || type == RENAME)
1094 				&& !oldMode.equals(newMode)) {
1095 			o.write(encodeASCII("old mode ")); //$NON-NLS-1$
1096 			oldMode.copyTo(o);
1097 			o.write('\n');
1098 
1099 			o.write(encodeASCII("new mode ")); //$NON-NLS-1$
1100 			newMode.copyTo(o);
1101 			o.write('\n');
1102 		}
1103 
1104 		switch (type) {
1105 		case ADD:
1106 			o.write(encodeASCII("new file mode ")); //$NON-NLS-1$
1107 			newMode.copyTo(o);
1108 			o.write('\n');
1109 			break;
1110 
1111 		case DELETE:
1112 			o.write(encodeASCII("deleted file mode ")); //$NON-NLS-1$
1113 			oldMode.copyTo(o);
1114 			o.write('\n');
1115 			break;
1116 
1117 		case RENAME:
1118 			o.write(encodeASCII("similarity index " + ent.getScore() + "%")); //$NON-NLS-1$ //$NON-NLS-2$
1119 			o.write('\n');
1120 
1121 			o.write(encode("rename from " + quotePath(oldp))); //$NON-NLS-1$
1122 			o.write('\n');
1123 
1124 			o.write(encode("rename to " + quotePath(newp))); //$NON-NLS-1$
1125 			o.write('\n');
1126 			break;
1127 
1128 		case COPY:
1129 			o.write(encodeASCII("similarity index " + ent.getScore() + "%")); //$NON-NLS-1$ //$NON-NLS-2$
1130 			o.write('\n');
1131 
1132 			o.write(encode("copy from " + quotePath(oldp))); //$NON-NLS-1$
1133 			o.write('\n');
1134 
1135 			o.write(encode("copy to " + quotePath(newp))); //$NON-NLS-1$
1136 			o.write('\n');
1137 			break;
1138 
1139 		case MODIFY:
1140 			if (0 < ent.getScore()) {
1141 				o.write(encodeASCII("dissimilarity index " //$NON-NLS-1$
1142 						+ (100 - ent.getScore()) + "%")); //$NON-NLS-1$
1143 				o.write('\n');
1144 			}
1145 			break;
1146 		}
1147 
1148 		if (ent.getOldId() != null && !ent.getOldId().equals(ent.getNewId())) {
1149 			formatIndexLine(o, ent);
1150 		}
1151 	}
1152 
1153 	/**
1154 	 * @param o
1155 	 *            the stream the formatter will write line data to
1156 	 * @param ent
1157 	 *            the DiffEntry to create the FileHeader for
1158 	 * @throws IOException
1159 	 *             writing to the supplied stream failed.
1160 	 */
1161 	protected void formatIndexLine(OutputStream o, DiffEntry ent)
1162 			throws IOException {
1163 		o.write(encodeASCII("index " // //$NON-NLS-1$
1164 				+ format(ent.getOldId()) //
1165 				+ ".." // //$NON-NLS-1$
1166 				+ format(ent.getNewId())));
1167 		if (ent.getOldMode().equals(ent.getNewMode())) {
1168 			o.write(' ');
1169 			ent.getNewMode().copyTo(o);
1170 		}
1171 		o.write('\n');
1172 	}
1173 
1174 	private void formatOldNewPaths(ByteArrayOutputStream o, DiffEntry ent)
1175 			throws IOException {
1176 		if (ent.oldId.equals(ent.newId))
1177 			return;
1178 
1179 		final String oldp;
1180 		final String newp;
1181 
1182 		switch (ent.getChangeType()) {
1183 		case ADD:
1184 			oldp = DiffEntry.DEV_NULL;
1185 			newp = quotePath(newPrefix + ent.getNewPath());
1186 			break;
1187 
1188 		case DELETE:
1189 			oldp = quotePath(oldPrefix + ent.getOldPath());
1190 			newp = DiffEntry.DEV_NULL;
1191 			break;
1192 
1193 		default:
1194 			oldp = quotePath(oldPrefix + ent.getOldPath());
1195 			newp = quotePath(newPrefix + ent.getNewPath());
1196 			break;
1197 		}
1198 
1199 		o.write(encode("--- " + oldp + "\n")); //$NON-NLS-1$ //$NON-NLS-2$
1200 		o.write(encode("+++ " + newp + "\n")); //$NON-NLS-1$ //$NON-NLS-2$
1201 	}
1202 
1203 	private int findCombinedEnd(final List<Edit> edits, final int i) {
1204 		int end = i + 1;
1205 		while (end < edits.size()
1206 				&& (combineA(edits, end) || combineB(edits, end)))
1207 			end++;
1208 		return end - 1;
1209 	}
1210 
1211 	private boolean combineA(final List<Edit> e, final int i) {
1212 		return e.get(i).getBeginA() - e.get(i - 1).getEndA() <= 2 * context;
1213 	}
1214 
1215 	private boolean combineB(final List<Edit> e, final int i) {
1216 		return e.get(i).getBeginB() - e.get(i - 1).getEndB() <= 2 * context;
1217 	}
1218 
1219 	private static boolean end(final Edit edit, final int a, final int b) {
1220 		return edit.getEndA() <= a && edit.getEndB() <= b;
1221 	}
1222 }