DiffFormatter.java

  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. package org.eclipse.jgit.diff;

  45. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
  46. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
  47. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
  48. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
  49. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
  50. import static org.eclipse.jgit.diff.DiffEntry.Side.NEW;
  51. import static org.eclipse.jgit.diff.DiffEntry.Side.OLD;
  52. import static org.eclipse.jgit.lib.Constants.encode;
  53. import static org.eclipse.jgit.lib.Constants.encodeASCII;
  54. import static org.eclipse.jgit.lib.FileMode.GITLINK;

  55. import java.io.ByteArrayOutputStream;
  56. import java.io.IOException;
  57. import java.io.OutputStream;
  58. import java.util.Collection;
  59. import java.util.Collections;
  60. import java.util.List;

  61. import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
  62. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  63. import org.eclipse.jgit.dircache.DirCacheIterator;
  64. import org.eclipse.jgit.errors.AmbiguousObjectException;
  65. import org.eclipse.jgit.errors.BinaryBlobException;
  66. import org.eclipse.jgit.errors.CancelledException;
  67. import org.eclipse.jgit.errors.CorruptObjectException;
  68. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  69. import org.eclipse.jgit.errors.MissingObjectException;
  70. import org.eclipse.jgit.internal.JGitText;
  71. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  72. import org.eclipse.jgit.lib.AnyObjectId;
  73. import org.eclipse.jgit.lib.Config;
  74. import org.eclipse.jgit.lib.ConfigConstants;
  75. import org.eclipse.jgit.lib.Constants;
  76. import org.eclipse.jgit.lib.FileMode;
  77. import org.eclipse.jgit.lib.ObjectId;
  78. import org.eclipse.jgit.lib.ObjectLoader;
  79. import org.eclipse.jgit.lib.ObjectReader;
  80. import org.eclipse.jgit.lib.ProgressMonitor;
  81. import org.eclipse.jgit.lib.Repository;
  82. import org.eclipse.jgit.patch.FileHeader;
  83. import org.eclipse.jgit.patch.FileHeader.PatchType;
  84. import org.eclipse.jgit.revwalk.FollowFilter;
  85. import org.eclipse.jgit.revwalk.RevTree;
  86. import org.eclipse.jgit.revwalk.RevWalk;
  87. import org.eclipse.jgit.storage.pack.PackConfig;
  88. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  89. import org.eclipse.jgit.treewalk.CanonicalTreeParser;
  90. import org.eclipse.jgit.treewalk.EmptyTreeIterator;
  91. import org.eclipse.jgit.treewalk.TreeWalk;
  92. import org.eclipse.jgit.treewalk.WorkingTreeIterator;
  93. import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
  94. import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
  95. import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
  96. import org.eclipse.jgit.treewalk.filter.PathFilter;
  97. import org.eclipse.jgit.treewalk.filter.TreeFilter;
  98. import org.eclipse.jgit.util.LfsFactory;
  99. import org.eclipse.jgit.util.QuotedString;

  100. /**
  101.  * Format a Git style patch script.
  102.  */
  103. public class DiffFormatter implements AutoCloseable {
  104.     private static final int DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;

  105.     private static final byte[] noNewLine = encodeASCII("\\ No newline at end of file\n"); //$NON-NLS-1$

  106.     /** Magic return content indicating it is empty or no content present. */
  107.     private static final byte[] EMPTY = new byte[] {};

  108.     private final OutputStream out;

  109.     private ObjectReader reader;

  110.     private boolean closeReader;

  111.     private DiffConfig diffCfg;

  112.     private int context = 3;

  113.     private int abbreviationLength = 7;

  114.     private DiffAlgorithm diffAlgorithm;

  115.     private RawTextComparator comparator = RawTextComparator.DEFAULT;

  116.     private int binaryFileThreshold = DEFAULT_BINARY_FILE_THRESHOLD;

  117.     private String oldPrefix = "a/"; //$NON-NLS-1$

  118.     private String newPrefix = "b/"; //$NON-NLS-1$

  119.     private TreeFilter pathFilter = TreeFilter.ALL;

  120.     private RenameDetector renameDetector;

  121.     private ProgressMonitor progressMonitor;

  122.     private ContentSource.Pair source;

  123.     private Repository repository;

  124.     /**
  125.      * Create a new formatter with a default level of context.
  126.      *
  127.      * @param out
  128.      *            the stream the formatter will write line data to. This stream
  129.      *            should have buffering arranged by the caller, as many small
  130.      *            writes are performed to it.
  131.      */
  132.     public DiffFormatter(OutputStream out) {
  133.         this.out = out;
  134.     }

  135.     /**
  136.      * Get output stream
  137.      *
  138.      * @return the stream we are outputting data to
  139.      */
  140.     protected OutputStream getOutputStream() {
  141.         return out;
  142.     }

  143.     /**
  144.      * Set the repository the formatter can load object contents from.
  145.      *
  146.      * Once a repository has been set, the formatter must be released to ensure
  147.      * the internal ObjectReader is able to release its resources.
  148.      *
  149.      * @param repository
  150.      *            source repository holding referenced objects.
  151.      */
  152.     public void setRepository(Repository repository) {
  153.         this.repository = repository;
  154.         setReader(repository.newObjectReader(), repository.getConfig(), true);
  155.     }

  156.     /**
  157.      * Set the repository the formatter can load object contents from.
  158.      *
  159.      * @param reader
  160.      *            source reader holding referenced objects. Caller is responsible
  161.      *            for closing the reader.
  162.      * @param cfg
  163.      *            config specifying diff algorithm and rename detection options.
  164.      * @since 4.5
  165.      */
  166.     public void setReader(ObjectReader reader, Config cfg) {
  167.         setReader(reader, cfg, false);
  168.     }

  169.     private void setReader(ObjectReader reader, Config cfg, boolean closeReader) {
  170.         close();
  171.         this.closeReader = closeReader;
  172.         this.reader = reader;
  173.         this.diffCfg = cfg.get(DiffConfig.KEY);

  174.         ContentSource cs = ContentSource.create(reader);
  175.         source = new ContentSource.Pair(cs, cs);

  176.         if (diffCfg.isNoPrefix()) {
  177.             setOldPrefix(""); //$NON-NLS-1$
  178.             setNewPrefix(""); //$NON-NLS-1$
  179.         }
  180.         setDetectRenames(diffCfg.isRenameDetectionEnabled());

  181.         diffAlgorithm = DiffAlgorithm.getAlgorithm(cfg.getEnum(
  182.                 ConfigConstants.CONFIG_DIFF_SECTION, null,
  183.                 ConfigConstants.CONFIG_KEY_ALGORITHM,
  184.                 SupportedAlgorithm.HISTOGRAM));
  185.     }

  186.     /**
  187.      * Change the number of lines of context to display.
  188.      *
  189.      * @param lineCount
  190.      *            number of lines of context to see before the first
  191.      *            modification and after the last modification within a hunk of
  192.      *            the modified file.
  193.      */
  194.     public void setContext(int lineCount) {
  195.         if (lineCount < 0)
  196.             throw new IllegalArgumentException(
  197.                     JGitText.get().contextMustBeNonNegative);
  198.         context = lineCount;
  199.     }

  200.     /**
  201.      * Change the number of digits to show in an ObjectId.
  202.      *
  203.      * @param count
  204.      *            number of digits to show in an ObjectId.
  205.      */
  206.     public void setAbbreviationLength(int count) {
  207.         if (count < 0)
  208.             throw new IllegalArgumentException(
  209.                     JGitText.get().abbreviationLengthMustBeNonNegative);
  210.         abbreviationLength = count;
  211.     }

  212.     /**
  213.      * Set the algorithm that constructs difference output.
  214.      *
  215.      * @param alg
  216.      *            the algorithm to produce text file differences.
  217.      * @see HistogramDiff
  218.      */
  219.     public void setDiffAlgorithm(DiffAlgorithm alg) {
  220.         diffAlgorithm = alg;
  221.     }

  222.     /**
  223.      * Set the line equivalence function for text file differences.
  224.      *
  225.      * @param cmp
  226.      *            The equivalence function used to determine if two lines of
  227.      *            text are identical. The function can be changed to ignore
  228.      *            various types of whitespace.
  229.      * @see RawTextComparator#DEFAULT
  230.      * @see RawTextComparator#WS_IGNORE_ALL
  231.      * @see RawTextComparator#WS_IGNORE_CHANGE
  232.      * @see RawTextComparator#WS_IGNORE_LEADING
  233.      * @see RawTextComparator#WS_IGNORE_TRAILING
  234.      */
  235.     public void setDiffComparator(RawTextComparator cmp) {
  236.         comparator = cmp;
  237.     }

  238.     /**
  239.      * Set maximum file size for text files.
  240.      *
  241.      * Files larger than this size will be treated as though they are binary and
  242.      * not text. Default is {@value #DEFAULT_BINARY_FILE_THRESHOLD} .
  243.      *
  244.      * @param threshold
  245.      *            the limit, in bytes. Files larger than this size will be
  246.      *            assumed to be binary, even if they aren't.
  247.      */
  248.     public void setBinaryFileThreshold(int threshold) {
  249.         this.binaryFileThreshold = threshold;
  250.     }

  251.     /**
  252.      * Set the prefix applied in front of old file paths.
  253.      *
  254.      * @param prefix
  255.      *            the prefix in front of old paths. Typically this is the
  256.      *            standard string {@code "a/"}, but may be any prefix desired by
  257.      *            the caller. Must not be null. Use the empty string to have no
  258.      *            prefix at all.
  259.      */
  260.     public void setOldPrefix(String prefix) {
  261.         oldPrefix = prefix;
  262.     }

  263.     /**
  264.      * Get the prefix applied in front of old file paths.
  265.      *
  266.      * @return the prefix
  267.      * @since 2.0
  268.      */
  269.     public String getOldPrefix() {
  270.         return this.oldPrefix;
  271.     }

  272.     /**
  273.      * Set the prefix applied in front of new file paths.
  274.      *
  275.      * @param prefix
  276.      *            the prefix in front of new paths. Typically this is the
  277.      *            standard string {@code "b/"}, but may be any prefix desired by
  278.      *            the caller. Must not be null. Use the empty string to have no
  279.      *            prefix at all.
  280.      */
  281.     public void setNewPrefix(String prefix) {
  282.         newPrefix = prefix;
  283.     }

  284.     /**
  285.      * Get the prefix applied in front of new file paths.
  286.      *
  287.      * @return the prefix
  288.      * @since 2.0
  289.      */
  290.     public String getNewPrefix() {
  291.         return this.newPrefix;
  292.     }

  293.     /**
  294.      * Get if rename detection is enabled
  295.      *
  296.      * @return true if rename detection is enabled
  297.      */
  298.     public boolean isDetectRenames() {
  299.         return renameDetector != null;
  300.     }

  301.     /**
  302.      * Enable or disable rename detection.
  303.      *
  304.      * Before enabling rename detection the repository must be set with
  305.      * {@link #setRepository(Repository)}. Once enabled the detector can be
  306.      * configured away from its defaults by obtaining the instance directly from
  307.      * {@link #getRenameDetector()} and invoking configuration.
  308.      *
  309.      * @param on
  310.      *            if rename detection should be enabled.
  311.      */
  312.     public void setDetectRenames(boolean on) {
  313.         if (on && renameDetector == null) {
  314.             assertHaveReader();
  315.             renameDetector = new RenameDetector(reader, diffCfg);
  316.         } else if (!on)
  317.             renameDetector = null;
  318.     }

  319.     /**
  320.      * Get rename detector
  321.      *
  322.      * @return the rename detector if rename detection is enabled
  323.      */
  324.     public RenameDetector getRenameDetector() {
  325.         return renameDetector;
  326.     }

  327.     /**
  328.      * Set the progress monitor for long running rename detection.
  329.      *
  330.      * @param pm
  331.      *            progress monitor to receive rename detection status through.
  332.      */
  333.     public void setProgressMonitor(ProgressMonitor pm) {
  334.         progressMonitor = pm;
  335.     }

  336.     /**
  337.      * Set the filter to produce only specific paths.
  338.      *
  339.      * If the filter is an instance of
  340.      * {@link org.eclipse.jgit.revwalk.FollowFilter}, the filter path will be
  341.      * updated during successive scan or format invocations. The updated path
  342.      * can be obtained from {@link #getPathFilter()}.
  343.      *
  344.      * @param filter
  345.      *            the tree filter to apply.
  346.      */
  347.     public void setPathFilter(TreeFilter filter) {
  348.         pathFilter = filter != null ? filter : TreeFilter.ALL;
  349.     }

  350.     /**
  351.      * Get path filter
  352.      *
  353.      * @return the current path filter
  354.      */
  355.     public TreeFilter getPathFilter() {
  356.         return pathFilter;
  357.     }

  358.     /**
  359.      * Flush the underlying output stream of this formatter.
  360.      *
  361.      * @throws java.io.IOException
  362.      *             the stream's own flush method threw an exception.
  363.      */
  364.     public void flush() throws IOException {
  365.         out.flush();
  366.     }

  367.     /**
  368.      * {@inheritDoc}
  369.      * <p>
  370.      * Release the internal ObjectReader state.
  371.      *
  372.      * @since 4.0
  373.      */
  374.     @Override
  375.     public void close() {
  376.         if (reader != null && closeReader) {
  377.             reader.close();
  378.         }
  379.     }

  380.     /**
  381.      * Determine the differences between two trees.
  382.      *
  383.      * No output is created, instead only the file paths that are different are
  384.      * returned. Callers may choose to format these paths themselves, or convert
  385.      * them into {@link org.eclipse.jgit.patch.FileHeader} instances with a
  386.      * complete edit list by calling {@link #toFileHeader(DiffEntry)}.
  387.      * <p>
  388.      * Either side may be null to indicate that the tree has beed added or
  389.      * removed. The diff will be computed against nothing.
  390.      *
  391.      * @param a
  392.      *            the old (or previous) side or null
  393.      * @param b
  394.      *            the new (or updated) side or null
  395.      * @return the paths that are different.
  396.      * @throws java.io.IOException
  397.      *             trees cannot be read or file contents cannot be read.
  398.      */
  399.     public List<DiffEntry> scan(AnyObjectId a, AnyObjectId b)
  400.             throws IOException {
  401.         assertHaveReader();

  402.         try (RevWalk rw = new RevWalk(reader)) {
  403.             RevTree aTree = a != null ? rw.parseTree(a) : null;
  404.             RevTree bTree = b != null ? rw.parseTree(b) : null;
  405.             return scan(aTree, bTree);
  406.         }
  407.     }

  408.     /**
  409.      * Determine the differences between two trees.
  410.      *
  411.      * No output is created, instead only the file paths that are different are
  412.      * returned. Callers may choose to format these paths themselves, or convert
  413.      * them into {@link org.eclipse.jgit.patch.FileHeader} instances with a
  414.      * complete edit list by calling {@link #toFileHeader(DiffEntry)}.
  415.      * <p>
  416.      * Either side may be null to indicate that the tree has beed added or
  417.      * removed. The diff will be computed against nothing.
  418.      *
  419.      * @param a
  420.      *            the old (or previous) side or null
  421.      * @param b
  422.      *            the new (or updated) side or null
  423.      * @return the paths that are different.
  424.      * @throws java.io.IOException
  425.      *             trees cannot be read or file contents cannot be read.
  426.      */
  427.     public List<DiffEntry> scan(RevTree a, RevTree b) throws IOException {
  428.         assertHaveReader();

  429.         AbstractTreeIterator aIterator = makeIteratorFromTreeOrNull(a);
  430.         AbstractTreeIterator bIterator = makeIteratorFromTreeOrNull(b);
  431.         return scan(aIterator, bIterator);
  432.     }

  433.     private AbstractTreeIterator makeIteratorFromTreeOrNull(RevTree tree)
  434.             throws IncorrectObjectTypeException, IOException {
  435.         if (tree != null) {
  436.             CanonicalTreeParser parser = new CanonicalTreeParser();
  437.             parser.reset(reader, tree);
  438.             return parser;
  439.         } else
  440.             return new EmptyTreeIterator();
  441.     }

  442.     /**
  443.      * Determine the differences between two trees.
  444.      *
  445.      * No output is created, instead only the file paths that are different are
  446.      * returned. Callers may choose to format these paths themselves, or convert
  447.      * them into {@link org.eclipse.jgit.patch.FileHeader} instances with a
  448.      * complete edit list by calling {@link #toFileHeader(DiffEntry)}.
  449.      *
  450.      * @param a
  451.      *            the old (or previous) side.
  452.      * @param b
  453.      *            the new (or updated) side.
  454.      * @return the paths that are different.
  455.      * @throws java.io.IOException
  456.      *             trees cannot be read or file contents cannot be read.
  457.      */
  458.     public List<DiffEntry> scan(AbstractTreeIterator a, AbstractTreeIterator b)
  459.             throws IOException {
  460.         assertHaveReader();

  461.         TreeWalk walk = new TreeWalk(reader);
  462.         walk.addTree(a);
  463.         walk.addTree(b);
  464.         walk.setRecursive(true);

  465.         TreeFilter filter = getDiffTreeFilterFor(a, b);
  466.         if (pathFilter instanceof FollowFilter) {
  467.             walk.setFilter(AndTreeFilter.create(
  468.                     PathFilter.create(((FollowFilter) pathFilter).getPath()),
  469.                     filter));
  470.         } else {
  471.             walk.setFilter(AndTreeFilter.create(pathFilter, filter));
  472.         }

  473.         source = new ContentSource.Pair(source(a), source(b));

  474.         List<DiffEntry> files = DiffEntry.scan(walk);
  475.         if (pathFilter instanceof FollowFilter && isAdd(files)) {
  476.             // The file we are following was added here, find where it
  477.             // came from so we can properly show the rename or copy,
  478.             // then continue digging backwards.
  479.             //
  480.             a.reset();
  481.             b.reset();
  482.             walk.reset();
  483.             walk.addTree(a);
  484.             walk.addTree(b);
  485.             walk.setFilter(filter);

  486.             if (renameDetector == null)
  487.                 setDetectRenames(true);
  488.             files = updateFollowFilter(detectRenames(DiffEntry.scan(walk)));

  489.         } else if (renameDetector != null)
  490.             files = detectRenames(files);

  491.         return files;
  492.     }

  493.     private static TreeFilter getDiffTreeFilterFor(AbstractTreeIterator a,
  494.             AbstractTreeIterator b) {
  495.         if (a instanceof DirCacheIterator && b instanceof WorkingTreeIterator)
  496.             return new IndexDiffFilter(0, 1);

  497.         if (a instanceof WorkingTreeIterator && b instanceof DirCacheIterator)
  498.             return new IndexDiffFilter(1, 0);

  499.         TreeFilter filter = TreeFilter.ANY_DIFF;
  500.         if (a instanceof WorkingTreeIterator)
  501.             filter = AndTreeFilter.create(new NotIgnoredFilter(0), filter);
  502.         if (b instanceof WorkingTreeIterator)
  503.             filter = AndTreeFilter.create(new NotIgnoredFilter(1), filter);
  504.         return filter;
  505.     }

  506.     private ContentSource source(AbstractTreeIterator iterator) {
  507.         if (iterator instanceof WorkingTreeIterator)
  508.             return ContentSource.create((WorkingTreeIterator) iterator);
  509.         return ContentSource.create(reader);
  510.     }

  511.     private List<DiffEntry> detectRenames(List<DiffEntry> files)
  512.             throws IOException {
  513.         renameDetector.reset();
  514.         renameDetector.addAll(files);
  515.         try {
  516.             return renameDetector.compute(reader, progressMonitor);
  517.         } catch (CancelledException e) {
  518.             // TODO: consider propagating once bug 536323 is tackled
  519.             // (making DiffEntry.scan() and DiffFormatter.scan() and
  520.             // format() cancellable).
  521.             return Collections.emptyList();
  522.         }
  523.     }

  524.     private boolean isAdd(List<DiffEntry> files) {
  525.         String oldPath = ((FollowFilter) pathFilter).getPath();
  526.         for (DiffEntry ent : files) {
  527.             if (ent.getChangeType() == ADD && ent.getNewPath().equals(oldPath))
  528.                 return true;
  529.         }
  530.         return false;
  531.     }

  532.     private List<DiffEntry> updateFollowFilter(List<DiffEntry> files) {
  533.         String oldPath = ((FollowFilter) pathFilter).getPath();
  534.         for (DiffEntry ent : files) {
  535.             if (isRename(ent) && ent.getNewPath().equals(oldPath)) {
  536.                 pathFilter = FollowFilter.create(ent.getOldPath(), diffCfg);
  537.                 return Collections.singletonList(ent);
  538.             }
  539.         }
  540.         return Collections.emptyList();
  541.     }

  542.     private static boolean isRename(DiffEntry ent) {
  543.         return ent.getChangeType() == RENAME || ent.getChangeType() == COPY;
  544.     }

  545.     /**
  546.      * Format the differences between two trees.
  547.      *
  548.      * The patch is expressed as instructions to modify {@code a} to make it
  549.      * {@code b}.
  550.      * <p>
  551.      * Either side may be null to indicate that the tree has beed added or
  552.      * removed. The diff will be computed against nothing.
  553.      *
  554.      * @param a
  555.      *            the old (or previous) side or null
  556.      * @param b
  557.      *            the new (or updated) side or null
  558.      * @throws java.io.IOException
  559.      *             trees cannot be read, file contents cannot be read, or the
  560.      *             patch cannot be output.
  561.      */
  562.     public void format(AnyObjectId a, AnyObjectId b) throws IOException {
  563.         format(scan(a, b));
  564.     }

  565.     /**
  566.      * Format the differences between two trees.
  567.      *
  568.      * The patch is expressed as instructions to modify {@code a} to make it
  569.      * {@code b}.
  570.      *
  571.      * <p>
  572.      * Either side may be null to indicate that the tree has beed added or
  573.      * removed. The diff will be computed against nothing.
  574.      *
  575.      * @param a
  576.      *            the old (or previous) side or null
  577.      * @param b
  578.      *            the new (or updated) side or null
  579.      * @throws java.io.IOException
  580.      *             trees cannot be read, file contents cannot be read, or the
  581.      *             patch cannot be output.
  582.      */
  583.     public void format(RevTree a, RevTree b) throws IOException {
  584.         format(scan(a, b));
  585.     }

  586.     /**
  587.      * Format the differences between two trees.
  588.      *
  589.      * The patch is expressed as instructions to modify {@code a} to make it
  590.      * {@code b}.
  591.      * <p>
  592.      * Either side may be null to indicate that the tree has beed added or
  593.      * removed. The diff will be computed against nothing.
  594.      *
  595.      * @param a
  596.      *            the old (or previous) side or null
  597.      * @param b
  598.      *            the new (or updated) side or null
  599.      * @throws java.io.IOException
  600.      *             trees cannot be read, file contents cannot be read, or the
  601.      *             patch cannot be output.
  602.      */
  603.     public void format(AbstractTreeIterator a, AbstractTreeIterator b)
  604.             throws IOException {
  605.         format(scan(a, b));
  606.     }

  607.     /**
  608.      * Format a patch script from a list of difference entries. Requires
  609.      * {@link #scan(AbstractTreeIterator, AbstractTreeIterator)} to have been
  610.      * called first.
  611.      *
  612.      * @param entries
  613.      *            entries describing the affected files.
  614.      * @throws java.io.IOException
  615.      *             a file's content cannot be read, or the output stream cannot
  616.      *             be written to.
  617.      */
  618.     public void format(List<? extends DiffEntry> entries) throws IOException {
  619.         for (DiffEntry ent : entries)
  620.             format(ent);
  621.     }

  622.     /**
  623.      * Format a patch script for one file entry.
  624.      *
  625.      * @param ent
  626.      *            the entry to be formatted.
  627.      * @throws java.io.IOException
  628.      *             a file's content cannot be read, or the output stream cannot
  629.      *             be written to.
  630.      */
  631.     public void format(DiffEntry ent) throws IOException {
  632.         FormatResult res = createFormatResult(ent);
  633.         format(res.header, res.a, res.b);
  634.     }

  635.     private static byte[] writeGitLinkText(AbbreviatedObjectId id) {
  636.         if (ObjectId.zeroId().equals(id.toObjectId())) {
  637.             return EMPTY;
  638.         }
  639.         return encodeASCII("Subproject commit " + id.name() //$NON-NLS-1$
  640.                 + "\n"); //$NON-NLS-1$
  641.     }

  642.     private String format(AbbreviatedObjectId id) {
  643.         if (id.isComplete() && reader != null) {
  644.             try {
  645.                 id = reader.abbreviate(id.toObjectId(), abbreviationLength);
  646.             } catch (IOException cannotAbbreviate) {
  647.                 // Ignore this. We'll report the full identity.
  648.             }
  649.         }
  650.         return id.name();
  651.     }

  652.     private static String quotePath(String name) {
  653.         return QuotedString.GIT_PATH.quote(name);
  654.     }

  655.     /**
  656.      * Format a patch script, reusing a previously parsed FileHeader.
  657.      * <p>
  658.      * This formatter is primarily useful for editing an existing patch script
  659.      * to increase or reduce the number of lines of context within the script.
  660.      * All header lines are reused as-is from the supplied FileHeader.
  661.      *
  662.      * @param head
  663.      *            existing file header containing the header lines to copy.
  664.      * @param a
  665.      *            text source for the pre-image version of the content. This
  666.      *            must match the content of
  667.      *            {@link org.eclipse.jgit.patch.FileHeader#getOldId()}.
  668.      * @param b
  669.      *            text source for the post-image version of the content. This
  670.      *            must match the content of
  671.      *            {@link org.eclipse.jgit.patch.FileHeader#getNewId()}.
  672.      * @throws java.io.IOException
  673.      *             writing to the supplied stream failed.
  674.      */
  675.     public void format(FileHeader head, RawText a, RawText b)
  676.             throws IOException {
  677.         // Reuse the existing FileHeader as-is by blindly copying its
  678.         // header lines, but avoiding its hunks. Instead we recreate
  679.         // the hunks from the text instances we have been supplied.
  680.         //
  681.         final int start = head.getStartOffset();
  682.         int end = head.getEndOffset();
  683.         if (!head.getHunks().isEmpty())
  684.             end = head.getHunks().get(0).getStartOffset();
  685.         out.write(head.getBuffer(), start, end - start);
  686.         if (head.getPatchType() == PatchType.UNIFIED)
  687.             format(head.toEditList(), a, b);
  688.     }

  689.     /**
  690.      * Formats a list of edits in unified diff format
  691.      *
  692.      * @param edits
  693.      *            some differences which have been calculated between A and B
  694.      * @param a
  695.      *            the text A which was compared
  696.      * @param b
  697.      *            the text B which was compared
  698.      * @throws java.io.IOException
  699.      */
  700.     public void format(EditList edits, RawText a, RawText b)
  701.             throws IOException {
  702.         for (int curIdx = 0; curIdx < edits.size();) {
  703.             Edit curEdit = edits.get(curIdx);
  704.             final int endIdx = findCombinedEnd(edits, curIdx);
  705.             final Edit endEdit = edits.get(endIdx);

  706.             int aCur = (int) Math.max(0, (long) curEdit.getBeginA() - context);
  707.             int bCur = (int) Math.max(0, (long) curEdit.getBeginB() - context);
  708.             final int aEnd = (int) Math.min(a.size(), (long) endEdit.getEndA() + context);
  709.             final int bEnd = (int) Math.min(b.size(), (long) endEdit.getEndB() + context);

  710.             writeHunkHeader(aCur, aEnd, bCur, bEnd);

  711.             while (aCur < aEnd || bCur < bEnd) {
  712.                 if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) {
  713.                     writeContextLine(a, aCur);
  714.                     if (isEndOfLineMissing(a, aCur))
  715.                         out.write(noNewLine);
  716.                     aCur++;
  717.                     bCur++;
  718.                 } else if (aCur < curEdit.getEndA()) {
  719.                     writeRemovedLine(a, aCur);
  720.                     if (isEndOfLineMissing(a, aCur))
  721.                         out.write(noNewLine);
  722.                     aCur++;
  723.                 } else if (bCur < curEdit.getEndB()) {
  724.                     writeAddedLine(b, bCur);
  725.                     if (isEndOfLineMissing(b, bCur))
  726.                         out.write(noNewLine);
  727.                     bCur++;
  728.                 }

  729.                 if (end(curEdit, aCur, bCur) && ++curIdx < edits.size())
  730.                     curEdit = edits.get(curIdx);
  731.             }
  732.         }
  733.     }

  734.     /**
  735.      * Output a line of context (unmodified line).
  736.      *
  737.      * @param text
  738.      *            RawText for accessing raw data
  739.      * @param line
  740.      *            the line number within text
  741.      * @throws java.io.IOException
  742.      */
  743.     protected void writeContextLine(RawText text, int line)
  744.             throws IOException {
  745.         writeLine(' ', text, line);
  746.     }

  747.     private static boolean isEndOfLineMissing(RawText text, int line) {
  748.         return line + 1 == text.size() && text.isMissingNewlineAtEnd();
  749.     }

  750.     /**
  751.      * Output an added line.
  752.      *
  753.      * @param text
  754.      *            RawText for accessing raw data
  755.      * @param line
  756.      *            the line number within text
  757.      * @throws java.io.IOException
  758.      */
  759.     protected void writeAddedLine(RawText text, int line)
  760.             throws IOException {
  761.         writeLine('+', text, line);
  762.     }

  763.     /**
  764.      * Output a removed line
  765.      *
  766.      * @param text
  767.      *            RawText for accessing raw data
  768.      * @param line
  769.      *            the line number within text
  770.      * @throws java.io.IOException
  771.      */
  772.     protected void writeRemovedLine(RawText text, int line)
  773.             throws IOException {
  774.         writeLine('-', text, line);
  775.     }

  776.     /**
  777.      * Output a hunk header
  778.      *
  779.      * @param aStartLine
  780.      *            within first source
  781.      * @param aEndLine
  782.      *            within first source
  783.      * @param bStartLine
  784.      *            within second source
  785.      * @param bEndLine
  786.      *            within second source
  787.      * @throws java.io.IOException
  788.      */
  789.     protected void writeHunkHeader(int aStartLine, int aEndLine,
  790.             int bStartLine, int bEndLine) throws IOException {
  791.         out.write('@');
  792.         out.write('@');
  793.         writeRange('-', aStartLine + 1, aEndLine - aStartLine);
  794.         writeRange('+', bStartLine + 1, bEndLine - bStartLine);
  795.         out.write(' ');
  796.         out.write('@');
  797.         out.write('@');
  798.         out.write('\n');
  799.     }

  800.     private void writeRange(char prefix, int begin, int cnt)
  801.             throws IOException {
  802.         out.write(' ');
  803.         out.write(prefix);
  804.         switch (cnt) {
  805.         case 0:
  806.             // If the range is empty, its beginning number must be the
  807.             // line just before the range, or 0 if the range is at the
  808.             // start of the file stream. Here, begin is always 1 based,
  809.             // so an empty file would produce "0,0".
  810.             //
  811.             out.write(encodeASCII(begin - 1));
  812.             out.write(',');
  813.             out.write('0');
  814.             break;

  815.         case 1:
  816.             // If the range is exactly one line, produce only the number.
  817.             //
  818.             out.write(encodeASCII(begin));
  819.             break;

  820.         default:
  821.             out.write(encodeASCII(begin));
  822.             out.write(',');
  823.             out.write(encodeASCII(cnt));
  824.             break;
  825.         }
  826.     }

  827.     /**
  828.      * Write a standard patch script line.
  829.      *
  830.      * @param prefix
  831.      *            prefix before the line, typically '-', '+', ' '.
  832.      * @param text
  833.      *            the text object to obtain the line from.
  834.      * @param cur
  835.      *            line number to output.
  836.      * @throws java.io.IOException
  837.      *             the stream threw an exception while writing to it.
  838.      */
  839.     protected void writeLine(final char prefix, final RawText text,
  840.             final int cur) throws IOException {
  841.         out.write(prefix);
  842.         text.writeLine(out, cur);
  843.         out.write('\n');
  844.     }

  845.     /**
  846.      * Creates a {@link org.eclipse.jgit.patch.FileHeader} representing the
  847.      * given {@link org.eclipse.jgit.diff.DiffEntry}
  848.      * <p>
  849.      * This method does not use the OutputStream associated with this
  850.      * DiffFormatter instance. It is therefore safe to instantiate this
  851.      * DiffFormatter instance with a
  852.      * {@link org.eclipse.jgit.util.io.DisabledOutputStream} if this method is
  853.      * the only one that will be used.
  854.      *
  855.      * @param ent
  856.      *            the DiffEntry to create the FileHeader for
  857.      * @return a FileHeader representing the DiffEntry. The FileHeader's buffer
  858.      *         will contain only the header of the diff output. It will also
  859.      *         contain one {@link org.eclipse.jgit.patch.HunkHeader}.
  860.      * @throws java.io.IOException
  861.      *             the stream threw an exception while writing to it, or one of
  862.      *             the blobs referenced by the DiffEntry could not be read.
  863.      * @throws org.eclipse.jgit.errors.CorruptObjectException
  864.      *             one of the blobs referenced by the DiffEntry is corrupt.
  865.      * @throws org.eclipse.jgit.errors.MissingObjectException
  866.      *             one of the blobs referenced by the DiffEntry is missing.
  867.      */
  868.     public FileHeader toFileHeader(DiffEntry ent) throws IOException,
  869.             CorruptObjectException, MissingObjectException {
  870.         return createFormatResult(ent).header;
  871.     }

  872.     private static class FormatResult {
  873.         FileHeader header;

  874.         RawText a;

  875.         RawText b;
  876.     }

  877.     private FormatResult createFormatResult(DiffEntry ent) throws IOException,
  878.             CorruptObjectException, MissingObjectException {
  879.         final FormatResult res = new FormatResult();
  880.         ByteArrayOutputStream buf = new ByteArrayOutputStream();
  881.         final EditList editList;
  882.         final FileHeader.PatchType type;

  883.         formatHeader(buf, ent);

  884.         if (ent.getOldId() == null || ent.getNewId() == null) {
  885.             // Content not changed (e.g. only mode, pure rename)
  886.             editList = new EditList();
  887.             type = PatchType.UNIFIED;
  888.             res.header = new FileHeader(buf.toByteArray(), editList, type);
  889.             return res;
  890.         }

  891.         assertHaveReader();

  892.         RawText aRaw = null;
  893.         RawText bRaw = null;
  894.         if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
  895.             aRaw = new RawText(writeGitLinkText(ent.getOldId()));
  896.             bRaw = new RawText(writeGitLinkText(ent.getNewId()));
  897.         } else {
  898.             try {
  899.                 aRaw = open(OLD, ent);
  900.                 bRaw = open(NEW, ent);
  901.             } catch (BinaryBlobException e) {
  902.                 // Do nothing; we check for null below.
  903.                 formatOldNewPaths(buf, ent);
  904.                 buf.write(encodeASCII("Binary files differ\n")); //$NON-NLS-1$
  905.                 editList = new EditList();
  906.                 type = PatchType.BINARY;
  907.                 res.header = new FileHeader(buf.toByteArray(), editList, type);
  908.                 return res;
  909.             }
  910.         }

  911.         res.a = aRaw;
  912.         res.b = bRaw;
  913.         editList = diff(res.a, res.b);
  914.         type = PatchType.UNIFIED;

  915.         switch (ent.getChangeType()) {
  916.             case RENAME:
  917.             case COPY:
  918.                 if (!editList.isEmpty())
  919.                     formatOldNewPaths(buf, ent);
  920.                 break;

  921.             default:
  922.                 formatOldNewPaths(buf, ent);
  923.                 break;
  924.         }


  925.         res.header = new FileHeader(buf.toByteArray(), editList, type);
  926.         return res;
  927.     }

  928.     private EditList diff(RawText a, RawText b) {
  929.         return diffAlgorithm.diff(comparator, a, b);
  930.     }

  931.     private void assertHaveReader() {
  932.         if (reader == null) {
  933.             throw new IllegalStateException(JGitText.get().readerIsRequired);
  934.         }
  935.     }

  936.     private RawText open(DiffEntry.Side side, DiffEntry entry)
  937.             throws IOException, BinaryBlobException {
  938.         if (entry.getMode(side) == FileMode.MISSING)
  939.             return RawText.EMPTY_TEXT;

  940.         if (entry.getMode(side).getObjectType() != Constants.OBJ_BLOB)
  941.             return RawText.EMPTY_TEXT;

  942.         AbbreviatedObjectId id = entry.getId(side);
  943.         if (!id.isComplete()) {
  944.             Collection<ObjectId> ids = reader.resolve(id);
  945.             if (ids.size() == 1) {
  946.                 id = AbbreviatedObjectId.fromObjectId(ids.iterator().next());
  947.                 switch (side) {
  948.                 case OLD:
  949.                     entry.oldId = id;
  950.                     break;
  951.                 case NEW:
  952.                     entry.newId = id;
  953.                     break;
  954.                 }
  955.             } else if (ids.size() == 0)
  956.                 throw new MissingObjectException(id, Constants.OBJ_BLOB);
  957.             else
  958.                 throw new AmbiguousObjectException(id, ids);
  959.         }

  960.         ObjectLoader ldr = LfsFactory.getInstance().applySmudgeFilter(repository,
  961.                 source.open(side, entry), entry.getDiffAttribute());
  962.         return RawText.load(ldr, binaryFileThreshold);
  963.     }

  964.     /**
  965.      * Output the first header line
  966.      *
  967.      * @param o
  968.      *            The stream the formatter will write the first header line to
  969.      * @param type
  970.      *            The {@link org.eclipse.jgit.diff.DiffEntry.ChangeType}
  971.      * @param oldPath
  972.      *            old path to the file
  973.      * @param newPath
  974.      *            new path to the file
  975.      * @throws java.io.IOException
  976.      *             the stream threw an exception while writing to it.
  977.      */
  978.     protected void formatGitDiffFirstHeaderLine(ByteArrayOutputStream o,
  979.             final ChangeType type, final String oldPath, final String newPath)
  980.             throws IOException {
  981.         o.write(encodeASCII("diff --git ")); //$NON-NLS-1$
  982.         o.write(encode(quotePath(oldPrefix + (type == ADD ? newPath : oldPath))));
  983.         o.write(' ');
  984.         o.write(encode(quotePath(newPrefix
  985.                 + (type == DELETE ? oldPath : newPath))));
  986.         o.write('\n');
  987.     }

  988.     private void formatHeader(ByteArrayOutputStream o, DiffEntry ent)
  989.             throws IOException {
  990.         final ChangeType type = ent.getChangeType();
  991.         final String oldp = ent.getOldPath();
  992.         final String newp = ent.getNewPath();
  993.         final FileMode oldMode = ent.getOldMode();
  994.         final FileMode newMode = ent.getNewMode();

  995.         formatGitDiffFirstHeaderLine(o, type, oldp, newp);

  996.         if ((type == MODIFY || type == COPY || type == RENAME)
  997.                 && !oldMode.equals(newMode)) {
  998.             o.write(encodeASCII("old mode ")); //$NON-NLS-1$
  999.             oldMode.copyTo(o);
  1000.             o.write('\n');

  1001.             o.write(encodeASCII("new mode ")); //$NON-NLS-1$
  1002.             newMode.copyTo(o);
  1003.             o.write('\n');
  1004.         }

  1005.         switch (type) {
  1006.         case ADD:
  1007.             o.write(encodeASCII("new file mode ")); //$NON-NLS-1$
  1008.             newMode.copyTo(o);
  1009.             o.write('\n');
  1010.             break;

  1011.         case DELETE:
  1012.             o.write(encodeASCII("deleted file mode ")); //$NON-NLS-1$
  1013.             oldMode.copyTo(o);
  1014.             o.write('\n');
  1015.             break;

  1016.         case RENAME:
  1017.             o.write(encodeASCII("similarity index " + ent.getScore() + "%")); //$NON-NLS-1$ //$NON-NLS-2$
  1018.             o.write('\n');

  1019.             o.write(encode("rename from " + quotePath(oldp))); //$NON-NLS-1$
  1020.             o.write('\n');

  1021.             o.write(encode("rename to " + quotePath(newp))); //$NON-NLS-1$
  1022.             o.write('\n');
  1023.             break;

  1024.         case COPY:
  1025.             o.write(encodeASCII("similarity index " + ent.getScore() + "%")); //$NON-NLS-1$ //$NON-NLS-2$
  1026.             o.write('\n');

  1027.             o.write(encode("copy from " + quotePath(oldp))); //$NON-NLS-1$
  1028.             o.write('\n');

  1029.             o.write(encode("copy to " + quotePath(newp))); //$NON-NLS-1$
  1030.             o.write('\n');
  1031.             break;

  1032.         case MODIFY:
  1033.             if (0 < ent.getScore()) {
  1034.                 o.write(encodeASCII("dissimilarity index " //$NON-NLS-1$
  1035.                         + (100 - ent.getScore()) + "%")); //$NON-NLS-1$
  1036.                 o.write('\n');
  1037.             }
  1038.             break;
  1039.         }

  1040.         if (ent.getOldId() != null && !ent.getOldId().equals(ent.getNewId())) {
  1041.             formatIndexLine(o, ent);
  1042.         }
  1043.     }

  1044.     /**
  1045.      * Format index line
  1046.      *
  1047.      * @param o
  1048.      *            the stream the formatter will write line data to
  1049.      * @param ent
  1050.      *            the DiffEntry to create the FileHeader for
  1051.      * @throws java.io.IOException
  1052.      *             writing to the supplied stream failed.
  1053.      */
  1054.     protected void formatIndexLine(OutputStream o, DiffEntry ent)
  1055.             throws IOException {
  1056.         o.write(encodeASCII("index " // //$NON-NLS-1$
  1057.                 + format(ent.getOldId()) //
  1058.                 + ".." // //$NON-NLS-1$
  1059.                 + format(ent.getNewId())));
  1060.         if (ent.getOldMode().equals(ent.getNewMode())) {
  1061.             o.write(' ');
  1062.             ent.getNewMode().copyTo(o);
  1063.         }
  1064.         o.write('\n');
  1065.     }

  1066.     private void formatOldNewPaths(ByteArrayOutputStream o, DiffEntry ent)
  1067.             throws IOException {
  1068.         if (ent.oldId.equals(ent.newId))
  1069.             return;

  1070.         final String oldp;
  1071.         final String newp;

  1072.         switch (ent.getChangeType()) {
  1073.         case ADD:
  1074.             oldp = DiffEntry.DEV_NULL;
  1075.             newp = quotePath(newPrefix + ent.getNewPath());
  1076.             break;

  1077.         case DELETE:
  1078.             oldp = quotePath(oldPrefix + ent.getOldPath());
  1079.             newp = DiffEntry.DEV_NULL;
  1080.             break;

  1081.         default:
  1082.             oldp = quotePath(oldPrefix + ent.getOldPath());
  1083.             newp = quotePath(newPrefix + ent.getNewPath());
  1084.             break;
  1085.         }

  1086.         o.write(encode("--- " + oldp + "\n")); //$NON-NLS-1$ //$NON-NLS-2$
  1087.         o.write(encode("+++ " + newp + "\n")); //$NON-NLS-1$ //$NON-NLS-2$
  1088.     }

  1089.     private int findCombinedEnd(List<Edit> edits, int i) {
  1090.         int end = i + 1;
  1091.         while (end < edits.size()
  1092.                 && (combineA(edits, end) || combineB(edits, end)))
  1093.             end++;
  1094.         return end - 1;
  1095.     }

  1096.     private boolean combineA(List<Edit> e, int i) {
  1097.         return e.get(i).getBeginA() - e.get(i - 1).getEndA() <= 2 * context;
  1098.     }

  1099.     private boolean combineB(List<Edit> e, int i) {
  1100.         return e.get(i).getBeginB() - e.get(i - 1).getEndB() <= 2 * context;
  1101.     }

  1102.     private static boolean end(Edit edit, int a, int b) {
  1103.         return edit.getEndA() <= a && edit.getEndB() <= b;
  1104.     }
  1105. }