DiffFormatter.java

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

  12. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
  13. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
  14. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
  15. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
  16. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
  17. import static org.eclipse.jgit.diff.DiffEntry.Side.NEW;
  18. import static org.eclipse.jgit.diff.DiffEntry.Side.OLD;
  19. import static org.eclipse.jgit.lib.Constants.encode;
  20. import static org.eclipse.jgit.lib.Constants.encodeASCII;
  21. import static org.eclipse.jgit.lib.FileMode.GITLINK;

  22. import java.io.ByteArrayOutputStream;
  23. import java.io.IOException;
  24. import java.io.OutputStream;
  25. import java.util.Collection;
  26. import java.util.Collections;
  27. import java.util.List;

  28. import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
  29. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  30. import org.eclipse.jgit.dircache.DirCacheIterator;
  31. import org.eclipse.jgit.errors.AmbiguousObjectException;
  32. import org.eclipse.jgit.errors.BinaryBlobException;
  33. import org.eclipse.jgit.errors.CancelledException;
  34. import org.eclipse.jgit.errors.CorruptObjectException;
  35. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  36. import org.eclipse.jgit.errors.MissingObjectException;
  37. import org.eclipse.jgit.internal.JGitText;
  38. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  39. import org.eclipse.jgit.lib.AnyObjectId;
  40. import org.eclipse.jgit.lib.Config;
  41. import org.eclipse.jgit.lib.ConfigConstants;
  42. import org.eclipse.jgit.lib.Constants;
  43. import org.eclipse.jgit.lib.FileMode;
  44. import org.eclipse.jgit.lib.ObjectId;
  45. import org.eclipse.jgit.lib.ObjectLoader;
  46. import org.eclipse.jgit.lib.ObjectReader;
  47. import org.eclipse.jgit.lib.ProgressMonitor;
  48. import org.eclipse.jgit.lib.Repository;
  49. import org.eclipse.jgit.patch.FileHeader;
  50. import org.eclipse.jgit.patch.FileHeader.PatchType;
  51. import org.eclipse.jgit.revwalk.FollowFilter;
  52. import org.eclipse.jgit.revwalk.RevTree;
  53. import org.eclipse.jgit.revwalk.RevWalk;
  54. import org.eclipse.jgit.storage.pack.PackConfig;
  55. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  56. import org.eclipse.jgit.treewalk.CanonicalTreeParser;
  57. import org.eclipse.jgit.treewalk.EmptyTreeIterator;
  58. import org.eclipse.jgit.treewalk.TreeWalk;
  59. import org.eclipse.jgit.treewalk.WorkingTreeIterator;
  60. import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
  61. import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
  62. import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
  63. import org.eclipse.jgit.treewalk.filter.PathFilter;
  64. import org.eclipse.jgit.treewalk.filter.TreeFilter;
  65. import org.eclipse.jgit.util.LfsFactory;
  66. import org.eclipse.jgit.util.QuotedString;

  67. /**
  68.  * Format a Git style patch script.
  69.  */
  70. public class DiffFormatter implements AutoCloseable {
  71.     private static final int DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;

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

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

  75.     private final OutputStream out;

  76.     private ObjectReader reader;

  77.     private boolean closeReader;

  78.     private DiffConfig diffCfg;

  79.     private int context = 3;

  80.     private int abbreviationLength = 7;

  81.     private DiffAlgorithm diffAlgorithm;

  82.     private RawTextComparator comparator = RawTextComparator.DEFAULT;

  83.     private int binaryFileThreshold = DEFAULT_BINARY_FILE_THRESHOLD;

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

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

  86.     private TreeFilter pathFilter = TreeFilter.ALL;

  87.     private RenameDetector renameDetector;

  88.     private ProgressMonitor progressMonitor;

  89.     private ContentSource.Pair source;

  90.     private Repository repository;

  91.     private Boolean quotePaths;

  92.     /**
  93.      * Create a new formatter with a default level of context.
  94.      *
  95.      * @param out
  96.      *            the stream the formatter will write line data to. This stream
  97.      *            should have buffering arranged by the caller, as many small
  98.      *            writes are performed to it.
  99.      */
  100.     public DiffFormatter(OutputStream out) {
  101.         this.out = out;
  102.     }

  103.     /**
  104.      * Get output stream
  105.      *
  106.      * @return the stream we are outputting data to
  107.      */
  108.     protected OutputStream getOutputStream() {
  109.         return out;
  110.     }

  111.     /**
  112.      * Set the repository the formatter can load object contents from.
  113.      *
  114.      * Once a repository has been set, the formatter must be released to ensure
  115.      * the internal ObjectReader is able to release its resources.
  116.      *
  117.      * @param repository
  118.      *            source repository holding referenced objects.
  119.      */
  120.     public void setRepository(Repository repository) {
  121.         this.repository = repository;
  122.         setReader(repository.newObjectReader(), repository.getConfig(), true);
  123.     }

  124.     /**
  125.      * Set the repository the formatter can load object contents from.
  126.      *
  127.      * @param reader
  128.      *            source reader holding referenced objects. Caller is responsible
  129.      *            for closing the reader.
  130.      * @param cfg
  131.      *            config specifying diff algorithm and rename detection options.
  132.      * @since 4.5
  133.      */
  134.     public void setReader(ObjectReader reader, Config cfg) {
  135.         setReader(reader, cfg, false);
  136.     }

  137.     private void setReader(ObjectReader reader, Config cfg, boolean closeReader) {
  138.         close();
  139.         this.closeReader = closeReader;
  140.         this.reader = reader;
  141.         this.diffCfg = cfg.get(DiffConfig.KEY);
  142.         if (quotePaths == null) {
  143.             quotePaths = Boolean
  144.                     .valueOf(cfg.getBoolean(ConfigConstants.CONFIG_CORE_SECTION,
  145.                             ConfigConstants.CONFIG_KEY_QUOTE_PATH, true));
  146.         }

  147.         ContentSource cs = ContentSource.create(reader);
  148.         source = new ContentSource.Pair(cs, cs);

  149.         if (diffCfg.isNoPrefix()) {
  150.             setOldPrefix(""); //$NON-NLS-1$
  151.             setNewPrefix(""); //$NON-NLS-1$
  152.         }
  153.         setDetectRenames(diffCfg.isRenameDetectionEnabled());

  154.         diffAlgorithm = DiffAlgorithm.getAlgorithm(cfg.getEnum(
  155.                 ConfigConstants.CONFIG_DIFF_SECTION, null,
  156.                 ConfigConstants.CONFIG_KEY_ALGORITHM,
  157.                 SupportedAlgorithm.HISTOGRAM));
  158.     }

  159.     /**
  160.      * Change the number of lines of context to display.
  161.      *
  162.      * @param lineCount
  163.      *            number of lines of context to see before the first
  164.      *            modification and after the last modification within a hunk of
  165.      *            the modified file.
  166.      */
  167.     public void setContext(int lineCount) {
  168.         if (lineCount < 0)
  169.             throw new IllegalArgumentException(
  170.                     JGitText.get().contextMustBeNonNegative);
  171.         context = lineCount;
  172.     }

  173.     /**
  174.      * Change the number of digits to show in an ObjectId.
  175.      *
  176.      * @param count
  177.      *            number of digits to show in an ObjectId.
  178.      */
  179.     public void setAbbreviationLength(int count) {
  180.         if (count < 0)
  181.             throw new IllegalArgumentException(
  182.                     JGitText.get().abbreviationLengthMustBeNonNegative);
  183.         abbreviationLength = count;
  184.     }

  185.     /**
  186.      * Set the algorithm that constructs difference output.
  187.      *
  188.      * @param alg
  189.      *            the algorithm to produce text file differences.
  190.      * @see HistogramDiff
  191.      */
  192.     public void setDiffAlgorithm(DiffAlgorithm alg) {
  193.         diffAlgorithm = alg;
  194.     }

  195.     /**
  196.      * Set the line equivalence function for text file differences.
  197.      *
  198.      * @param cmp
  199.      *            The equivalence function used to determine if two lines of
  200.      *            text are identical. The function can be changed to ignore
  201.      *            various types of whitespace.
  202.      * @see RawTextComparator#DEFAULT
  203.      * @see RawTextComparator#WS_IGNORE_ALL
  204.      * @see RawTextComparator#WS_IGNORE_CHANGE
  205.      * @see RawTextComparator#WS_IGNORE_LEADING
  206.      * @see RawTextComparator#WS_IGNORE_TRAILING
  207.      */
  208.     public void setDiffComparator(RawTextComparator cmp) {
  209.         comparator = cmp;
  210.     }

  211.     /**
  212.      * Set maximum file size for text files.
  213.      *
  214.      * Files larger than this size will be treated as though they are binary and
  215.      * not text. Default is {@value #DEFAULT_BINARY_FILE_THRESHOLD} .
  216.      *
  217.      * @param threshold
  218.      *            the limit, in bytes. Files larger than this size will be
  219.      *            assumed to be binary, even if they aren't.
  220.      */
  221.     public void setBinaryFileThreshold(int threshold) {
  222.         this.binaryFileThreshold = threshold;
  223.     }

  224.     /**
  225.      * Set the prefix applied in front of old file paths.
  226.      *
  227.      * @param prefix
  228.      *            the prefix in front of old paths. Typically this is the
  229.      *            standard string {@code "a/"}, but may be any prefix desired by
  230.      *            the caller. Must not be null. Use the empty string to have no
  231.      *            prefix at all.
  232.      */
  233.     public void setOldPrefix(String prefix) {
  234.         oldPrefix = prefix;
  235.     }

  236.     /**
  237.      * Get the prefix applied in front of old file paths.
  238.      *
  239.      * @return the prefix
  240.      * @since 2.0
  241.      */
  242.     public String getOldPrefix() {
  243.         return this.oldPrefix;
  244.     }

  245.     /**
  246.      * Set the prefix applied in front of new file paths.
  247.      *
  248.      * @param prefix
  249.      *            the prefix in front of new paths. Typically this is the
  250.      *            standard string {@code "b/"}, but may be any prefix desired by
  251.      *            the caller. Must not be null. Use the empty string to have no
  252.      *            prefix at all.
  253.      */
  254.     public void setNewPrefix(String prefix) {
  255.         newPrefix = prefix;
  256.     }

  257.     /**
  258.      * Get the prefix applied in front of new file paths.
  259.      *
  260.      * @return the prefix
  261.      * @since 2.0
  262.      */
  263.     public String getNewPrefix() {
  264.         return this.newPrefix;
  265.     }

  266.     /**
  267.      * Get if rename detection is enabled
  268.      *
  269.      * @return true if rename detection is enabled
  270.      */
  271.     public boolean isDetectRenames() {
  272.         return renameDetector != null;
  273.     }

  274.     /**
  275.      * Enable or disable rename detection.
  276.      *
  277.      * Before enabling rename detection the repository must be set with
  278.      * {@link #setRepository(Repository)}. Once enabled the detector can be
  279.      * configured away from its defaults by obtaining the instance directly from
  280.      * {@link #getRenameDetector()} and invoking configuration.
  281.      *
  282.      * @param on
  283.      *            if rename detection should be enabled.
  284.      */
  285.     public void setDetectRenames(boolean on) {
  286.         if (on && renameDetector == null) {
  287.             assertHaveReader();
  288.             renameDetector = new RenameDetector(reader, diffCfg);
  289.         } else if (!on)
  290.             renameDetector = null;
  291.     }

  292.     /**
  293.      * Get rename detector
  294.      *
  295.      * @return the rename detector if rename detection is enabled
  296.      */
  297.     public RenameDetector getRenameDetector() {
  298.         return renameDetector;
  299.     }

  300.     /**
  301.      * Set the progress monitor for long running rename detection.
  302.      *
  303.      * @param pm
  304.      *            progress monitor to receive rename detection status through.
  305.      */
  306.     public void setProgressMonitor(ProgressMonitor pm) {
  307.         progressMonitor = pm;
  308.     }

  309.     /**
  310.      * Sets whether or not path names should be quoted.
  311.      * <p>
  312.      * By default the setting of git config {@code core.quotePath} is active,
  313.      * but this can be overridden through this method.
  314.      * </p>
  315.      *
  316.      * @param quote
  317.      *            whether to quote path names
  318.      * @since 5.6
  319.      */
  320.     public void setQuotePaths(boolean quote) {
  321.         quotePaths = Boolean.valueOf(quote);
  322.     }

  323.     /**
  324.      * Set the filter to produce only specific paths.
  325.      *
  326.      * If the filter is an instance of
  327.      * {@link org.eclipse.jgit.revwalk.FollowFilter}, the filter path will be
  328.      * updated during successive scan or format invocations. The updated path
  329.      * can be obtained from {@link #getPathFilter()}.
  330.      *
  331.      * @param filter
  332.      *            the tree filter to apply.
  333.      */
  334.     public void setPathFilter(TreeFilter filter) {
  335.         pathFilter = filter != null ? filter : TreeFilter.ALL;
  336.     }

  337.     /**
  338.      * Get path filter
  339.      *
  340.      * @return the current path filter
  341.      */
  342.     public TreeFilter getPathFilter() {
  343.         return pathFilter;
  344.     }

  345.     /**
  346.      * Flush the underlying output stream of this formatter.
  347.      *
  348.      * @throws java.io.IOException
  349.      *             the stream's own flush method threw an exception.
  350.      */
  351.     public void flush() throws IOException {
  352.         out.flush();
  353.     }

  354.     /**
  355.      * {@inheritDoc}
  356.      * <p>
  357.      * Release the internal ObjectReader state.
  358.      *
  359.      * @since 4.0
  360.      */
  361.     @Override
  362.     public void close() {
  363.         if (reader != null && closeReader) {
  364.             reader.close();
  365.         }
  366.     }

  367.     /**
  368.      * Determine the differences between two trees.
  369.      *
  370.      * No output is created, instead only the file paths that are different are
  371.      * returned. Callers may choose to format these paths themselves, or convert
  372.      * them into {@link org.eclipse.jgit.patch.FileHeader} instances with a
  373.      * complete edit list by calling {@link #toFileHeader(DiffEntry)}.
  374.      * <p>
  375.      * Either side may be null to indicate that the tree has beed added or
  376.      * removed. The diff will be computed against nothing.
  377.      *
  378.      * @param a
  379.      *            the old (or previous) side or null
  380.      * @param b
  381.      *            the new (or updated) side or null
  382.      * @return the paths that are different.
  383.      * @throws java.io.IOException
  384.      *             trees cannot be read or file contents cannot be read.
  385.      */
  386.     public List<DiffEntry> scan(AnyObjectId a, AnyObjectId b)
  387.             throws IOException {
  388.         assertHaveReader();

  389.         try (RevWalk rw = new RevWalk(reader)) {
  390.             RevTree aTree = a != null ? rw.parseTree(a) : null;
  391.             RevTree bTree = b != null ? rw.parseTree(b) : null;
  392.             return scan(aTree, bTree);
  393.         }
  394.     }

  395.     /**
  396.      * Determine the differences between two trees.
  397.      *
  398.      * No output is created, instead only the file paths that are different are
  399.      * returned. Callers may choose to format these paths themselves, or convert
  400.      * them into {@link org.eclipse.jgit.patch.FileHeader} instances with a
  401.      * complete edit list by calling {@link #toFileHeader(DiffEntry)}.
  402.      * <p>
  403.      * Either side may be null to indicate that the tree has beed added or
  404.      * removed. The diff will be computed against nothing.
  405.      *
  406.      * @param a
  407.      *            the old (or previous) side or null
  408.      * @param b
  409.      *            the new (or updated) side or null
  410.      * @return the paths that are different.
  411.      * @throws java.io.IOException
  412.      *             trees cannot be read or file contents cannot be read.
  413.      */
  414.     public List<DiffEntry> scan(RevTree a, RevTree b) throws IOException {
  415.         assertHaveReader();

  416.         AbstractTreeIterator aIterator = makeIteratorFromTreeOrNull(a);
  417.         AbstractTreeIterator bIterator = makeIteratorFromTreeOrNull(b);
  418.         return scan(aIterator, bIterator);
  419.     }

  420.     private AbstractTreeIterator makeIteratorFromTreeOrNull(RevTree tree)
  421.             throws IncorrectObjectTypeException, IOException {
  422.         if (tree != null) {
  423.             CanonicalTreeParser parser = new CanonicalTreeParser();
  424.             parser.reset(reader, tree);
  425.             return parser;
  426.         }
  427.         return new EmptyTreeIterator();
  428.     }

  429.     /**
  430.      * Determine the differences between two trees.
  431.      *
  432.      * No output is created, instead only the file paths that are different are
  433.      * returned. Callers may choose to format these paths themselves, or convert
  434.      * them into {@link org.eclipse.jgit.patch.FileHeader} instances with a
  435.      * complete edit list by calling {@link #toFileHeader(DiffEntry)}.
  436.      *
  437.      * @param a
  438.      *            the old (or previous) side.
  439.      * @param b
  440.      *            the new (or updated) side.
  441.      * @return the paths that are different.
  442.      * @throws java.io.IOException
  443.      *             trees cannot be read or file contents cannot be read.
  444.      */
  445.     public List<DiffEntry> scan(AbstractTreeIterator a, AbstractTreeIterator b)
  446.             throws IOException {
  447.         assertHaveReader();

  448.         TreeWalk walk = new TreeWalk(repository, reader);
  449.         int aIndex = walk.addTree(a);
  450.         int bIndex = walk.addTree(b);
  451.         if (repository != null) {
  452.             if (a instanceof WorkingTreeIterator
  453.                     && b instanceof DirCacheIterator) {
  454.                 ((WorkingTreeIterator) a).setDirCacheIterator(walk, bIndex);
  455.             } else if (b instanceof WorkingTreeIterator
  456.                     && a instanceof DirCacheIterator) {
  457.                 ((WorkingTreeIterator) b).setDirCacheIterator(walk, aIndex);
  458.             }
  459.         }
  460.         walk.setRecursive(true);

  461.         TreeFilter filter = getDiffTreeFilterFor(a, b);
  462.         if (pathFilter instanceof FollowFilter) {
  463.             walk.setFilter(AndTreeFilter.create(
  464.                     PathFilter.create(((FollowFilter) pathFilter).getPath()),
  465.                     filter));
  466.         } else {
  467.             walk.setFilter(AndTreeFilter.create(pathFilter, filter));
  468.         }

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

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

  482.             if (renameDetector == null)
  483.                 setDetectRenames(true);
  484.             files = updateFollowFilter(detectRenames(DiffEntry.scan(walk)));

  485.         } else if (renameDetector != null)
  486.             files = detectRenames(files);

  487.         return files;
  488.     }

  489.     private static TreeFilter getDiffTreeFilterFor(AbstractTreeIterator a,
  490.             AbstractTreeIterator b) {
  491.         if (a instanceof DirCacheIterator && b instanceof WorkingTreeIterator)
  492.             return new IndexDiffFilter(0, 1);

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

  495.         TreeFilter filter = TreeFilter.ANY_DIFF;
  496.         if (a instanceof WorkingTreeIterator)
  497.             filter = AndTreeFilter.create(new NotIgnoredFilter(0), filter);
  498.         if (b instanceof WorkingTreeIterator)
  499.             filter = AndTreeFilter.create(new NotIgnoredFilter(1), filter);
  500.         return filter;
  501.     }

  502.     private ContentSource source(AbstractTreeIterator iterator) {
  503.         if (iterator instanceof WorkingTreeIterator)
  504.             return ContentSource.create((WorkingTreeIterator) iterator);
  505.         return ContentSource.create(reader);
  506.     }

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

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

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

  538.     private static boolean isRename(DiffEntry ent) {
  539.         return ent.getChangeType() == RENAME || ent.getChangeType() == COPY;
  540.     }

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

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

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

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

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

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

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

  648.     private String quotePath(String path) {
  649.         if (quotePaths == null || quotePaths.booleanValue()) {
  650.             return QuotedString.GIT_PATH.quote(path);
  651.         }
  652.         return QuotedString.GIT_PATH_MINIMAL.quote(path);
  653.     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  873.         RawText a;

  874.         RawText b;
  875.     }

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

  882.         formatHeader(buf, ent);

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

  890.         assertHaveReader();

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1069.         final String oldp;
  1070.         final String newp;

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

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

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

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

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

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

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

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