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