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