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.OBJECT_ID_ABBREV_STRING_LENGTH;
  20. import static org.eclipse.jgit.lib.Constants.encode;
  21. import static org.eclipse.jgit.lib.Constants.encodeASCII;
  22. import static org.eclipse.jgit.lib.FileMode.GITLINK;

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

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

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

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

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

  76.     private final OutputStream out;

  77.     private ObjectReader reader;

  78.     private boolean closeReader;

  79.     private DiffConfig diffCfg;

  80.     private int context = 3;

  81.     private int abbreviationLength = OBJECT_ID_ABBREV_STRING_LENGTH;

  82.     private DiffAlgorithm diffAlgorithm;

  83.     private RawTextComparator comparator = RawTextComparator.DEFAULT;

  84.     private int binaryFileThreshold = DEFAULT_BINARY_FILE_THRESHOLD;

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

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

  87.     private TreeFilter pathFilter = TreeFilter.ALL;

  88.     private RenameDetector renameDetector;

  89.     private ProgressMonitor progressMonitor;

  90.     private ContentSource.Pair source;

  91.     private Repository repository;

  92.     private Boolean quotePaths;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  488.         return files;
  489.     }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  649.     private String quotePath(String path) {
  650.         if (quotePaths == null || quotePaths.booleanValue()) {
  651.             return QuotedString.GIT_PATH.quote(path);
  652.         }
  653.         return QuotedString.GIT_PATH_MINIMAL.quote(path);
  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.isEmpty())
  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. }