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