PatchApplier.java

  1. /*
  2.  * Copyright (C) 2022, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.patch;

  11. import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;

  12. import java.io.ByteArrayInputStream;
  13. import java.io.File;
  14. import java.io.FileInputStream;
  15. import java.io.IOException;
  16. import java.io.InputStream;
  17. import java.io.OutputStream;
  18. import java.nio.ByteBuffer;
  19. import java.nio.charset.StandardCharsets;
  20. import java.nio.file.Files;
  21. import java.nio.file.StandardCopyOption;
  22. import java.text.MessageFormat;
  23. import java.time.Instant;
  24. import java.util.ArrayList;
  25. import java.util.HashSet;
  26. import java.util.Iterator;
  27. import java.util.List;
  28. import java.util.Set;
  29. import java.util.stream.Collectors;
  30. import java.util.zip.InflaterInputStream;
  31. import org.eclipse.jgit.annotations.Nullable;
  32. import org.eclipse.jgit.api.errors.FilterFailedException;
  33. import org.eclipse.jgit.api.errors.PatchApplyException;
  34. import org.eclipse.jgit.api.errors.PatchFormatException;
  35. import org.eclipse.jgit.attributes.Attribute;
  36. import org.eclipse.jgit.attributes.Attributes;
  37. import org.eclipse.jgit.attributes.FilterCommand;
  38. import org.eclipse.jgit.attributes.FilterCommandRegistry;
  39. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  40. import org.eclipse.jgit.diff.RawText;
  41. import org.eclipse.jgit.dircache.DirCache;
  42. import org.eclipse.jgit.dircache.DirCacheBuilder;
  43. import org.eclipse.jgit.dircache.DirCacheCheckout;
  44. import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
  45. import org.eclipse.jgit.dircache.DirCacheCheckout.StreamSupplier;
  46. import org.eclipse.jgit.dircache.DirCacheEntry;
  47. import org.eclipse.jgit.dircache.DirCacheIterator;
  48. import org.eclipse.jgit.errors.IndexWriteException;
  49. import org.eclipse.jgit.internal.JGitText;
  50. import org.eclipse.jgit.lib.Config;
  51. import org.eclipse.jgit.lib.ConfigConstants;
  52. import org.eclipse.jgit.lib.Constants;
  53. import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
  54. import org.eclipse.jgit.lib.FileMode;
  55. import org.eclipse.jgit.lib.ObjectId;
  56. import org.eclipse.jgit.lib.ObjectInserter;
  57. import org.eclipse.jgit.lib.ObjectLoader;
  58. import org.eclipse.jgit.lib.ObjectReader;
  59. import org.eclipse.jgit.lib.Repository;
  60. import org.eclipse.jgit.patch.FileHeader.PatchType;
  61. import org.eclipse.jgit.revwalk.RevTree;
  62. import org.eclipse.jgit.treewalk.FileTreeIterator;
  63. import org.eclipse.jgit.treewalk.TreeWalk;
  64. import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
  65. import org.eclipse.jgit.treewalk.WorkingTreeOptions;
  66. import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
  67. import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
  68. import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
  69. import org.eclipse.jgit.util.FS.ExecutionResult;
  70. import org.eclipse.jgit.util.FileUtils;
  71. import org.eclipse.jgit.util.IO;
  72. import org.eclipse.jgit.util.LfsFactory;
  73. import org.eclipse.jgit.util.LfsFactory.LfsInputStream;
  74. import org.eclipse.jgit.util.RawParseUtils;
  75. import org.eclipse.jgit.util.TemporaryBuffer;
  76. import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
  77. import org.eclipse.jgit.util.io.BinaryDeltaInputStream;
  78. import org.eclipse.jgit.util.io.BinaryHunkInputStream;
  79. import org.eclipse.jgit.util.io.EolStreamTypeUtil;
  80. import org.eclipse.jgit.util.sha1.SHA1;

  81. /**
  82.  * Applies a patch to files and the index.
  83.  * <p>
  84.  * After instantiating, applyPatch() should be called once.
  85.  * </p>
  86.  *
  87.  * @since 6.4
  88.  */
  89. public class PatchApplier {

  90.     /** The tree before applying the patch. Only non-null for inCore operation. */
  91.     @Nullable
  92.     private final RevTree beforeTree;

  93.     private final Repository repo;

  94.     private final ObjectInserter inserter;

  95.     private final ObjectReader reader;

  96.     private WorkingTreeOptions workingTreeOptions;

  97.     private int inCoreSizeLimit;

  98.     /**
  99.      * @param repo
  100.      *            repository to apply the patch in
  101.      */
  102.     public PatchApplier(Repository repo) {
  103.         this.repo = repo;
  104.         inserter = repo.newObjectInserter();
  105.         reader = inserter.newReader();
  106.         beforeTree = null;

  107.         Config config = repo.getConfig();
  108.         workingTreeOptions = config.get(WorkingTreeOptions.KEY);
  109.         inCoreSizeLimit = config.getInt(ConfigConstants.CONFIG_MERGE_SECTION,
  110.                 ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20);
  111.     }

  112.     /**
  113.      * @param repo
  114.      *            repository to apply the patch in
  115.      * @param beforeTree
  116.      *            ID of the tree to apply the patch in
  117.      * @param oi
  118.      *            to be used for modifying objects
  119.      * @throws IOException
  120.      *             in case of I/O errors
  121.      */
  122.     public PatchApplier(Repository repo, RevTree beforeTree, ObjectInserter oi)
  123.             throws IOException {
  124.         this.repo = repo;
  125.         this.beforeTree = beforeTree;
  126.         inserter = oi;
  127.         reader = oi.newReader();
  128.     }

  129.     /**
  130.      * A wrapper for returning both the applied tree ID and the applied files
  131.      * list.
  132.      *
  133.      * @since 6.3
  134.      */
  135.     public static class Result {

  136.         private ObjectId treeId;

  137.         private List<String> paths;

  138.         /**
  139.          * @return List of modified paths.
  140.          */
  141.         public List<String> getPaths() {
  142.             return paths;
  143.         }

  144.         /**
  145.          * @return The applied tree ID.
  146.          */
  147.         public ObjectId getTreeId() {
  148.             return treeId;
  149.         }
  150.     }

  151.     /**
  152.      * Applies the given patch
  153.      *
  154.      * @param patchInput
  155.      *            the patch to apply.
  156.      * @return the result of the patch
  157.      * @throws PatchFormatException
  158.      *             if the patch cannot be parsed
  159.      * @throws PatchApplyException
  160.      *             if the patch cannot be applied
  161.      */
  162.     public Result applyPatch(InputStream patchInput)
  163.             throws PatchFormatException, PatchApplyException {
  164.         Result result = new Result();
  165.         org.eclipse.jgit.patch.Patch p = new org.eclipse.jgit.patch.Patch();
  166.         try (InputStream inStream = patchInput) {
  167.             p.parse(inStream);

  168.             if (!p.getErrors().isEmpty()) {
  169.                 throw new PatchFormatException(p.getErrors());
  170.             }

  171.             DirCache dirCache = (inCore()) ? DirCache.newInCore()
  172.                     : repo.lockDirCache();

  173.             DirCacheBuilder dirCacheBuilder = dirCache.builder();
  174.             Set<String> modifiedPaths = new HashSet<>();
  175.             for (org.eclipse.jgit.patch.FileHeader fh : p.getFiles()) {
  176.                 ChangeType type = fh.getChangeType();
  177.                 switch (type) {
  178.                 case ADD: {
  179.                     File f = getFile(fh.getNewPath());
  180.                     if (f != null) {
  181.                         try {
  182.                             FileUtils.mkdirs(f.getParentFile(), true);
  183.                             FileUtils.createNewFile(f);
  184.                         } catch (IOException e) {
  185.                             throw new PatchApplyException(MessageFormat.format(
  186.                                     JGitText.get().createNewFileFailed, f), e);
  187.                         }
  188.                     }
  189.                     apply(fh.getNewPath(), dirCache, dirCacheBuilder, f, fh);
  190.                 }
  191.                     break;
  192.                 case MODIFY:
  193.                     apply(fh.getOldPath(), dirCache, dirCacheBuilder,
  194.                             getFile(fh.getOldPath()), fh);
  195.                     break;
  196.                 case DELETE:
  197.                     if (!inCore()) {
  198.                         File old = getFile(fh.getOldPath());
  199.                         if (!old.delete())
  200.                             throw new PatchApplyException(MessageFormat.format(
  201.                                     JGitText.get().cannotDeleteFile, old));
  202.                     }
  203.                     break;
  204.                 case RENAME: {
  205.                     File src = getFile(fh.getOldPath());
  206.                     File dest = getFile(fh.getNewPath());

  207.                     if (!inCore()) {
  208.                         /*
  209.                          * this is odd: we rename the file on the FS, but
  210.                          * apply() will write a fresh stream anyway, which will
  211.                          * overwrite if there were hunks in the patch.
  212.                          */
  213.                         try {
  214.                             FileUtils.mkdirs(dest.getParentFile(), true);
  215.                             FileUtils.rename(src, dest,
  216.                                     StandardCopyOption.ATOMIC_MOVE);
  217.                         } catch (IOException e) {
  218.                             throw new PatchApplyException(MessageFormat.format(
  219.                                     JGitText.get().renameFileFailed, src, dest),
  220.                                     e);
  221.                         }
  222.                     }
  223.                     String pathWithOriginalContent = inCore() ?
  224.                             fh.getOldPath() : fh.getNewPath();
  225.                     apply(pathWithOriginalContent, dirCache, dirCacheBuilder, dest, fh);
  226.                     break;
  227.                 }
  228.                 case COPY: {
  229.                     File dest = getFile(fh.getNewPath());
  230.                     if (!inCore()) {
  231.                         File src = getFile(fh.getOldPath());
  232.                         FileUtils.mkdirs(dest.getParentFile(), true);
  233.                         Files.copy(src.toPath(), dest.toPath());
  234.                     }
  235.                     apply(fh.getOldPath(), dirCache, dirCacheBuilder, dest, fh);
  236.                     break;
  237.                 }
  238.                 }
  239.                 if (fh.getChangeType() != ChangeType.DELETE)
  240.                     modifiedPaths.add(fh.getNewPath());
  241.                 if (fh.getChangeType() != ChangeType.COPY
  242.                         && fh.getChangeType() != ChangeType.ADD)
  243.                     modifiedPaths.add(fh.getOldPath());
  244.             }

  245.             // We processed the patch. Now add things that weren't changed.
  246.             for (int i = 0; i < dirCache.getEntryCount(); i++) {
  247.                 DirCacheEntry dce = dirCache.getEntry(i);
  248.                 if (!modifiedPaths.contains(dce.getPathString())
  249.                         || dce.getStage() != DirCacheEntry.STAGE_0)
  250.                     dirCacheBuilder.add(dce);
  251.             }

  252.             if (inCore())
  253.                 dirCacheBuilder.finish();
  254.             else if (!dirCacheBuilder.commit()) {
  255.                 throw new IndexWriteException();
  256.             }

  257.             result.treeId = dirCache.writeTree(inserter);
  258.             result.paths = modifiedPaths.stream().sorted()
  259.                     .collect(Collectors.toList());
  260.         } catch (IOException e) {
  261.             throw new PatchApplyException(MessageFormat.format(
  262.                     JGitText.get().patchApplyException, e.getMessage()), e);
  263.         }
  264.         return result;
  265.     }

  266.     private File getFile(String path) {
  267.         return (inCore()) ? null : new File(repo.getWorkTree(), path);
  268.     }

  269.     /* returns null if the path is not found. */
  270.     @Nullable
  271.     private TreeWalk getTreeWalkForFile(String path, DirCache cache)
  272.             throws PatchApplyException {
  273.         try {
  274.             if (inCore()) {
  275.                 // Only this branch may return null.
  276.                 // TODO: it would be nice if we could return a TreeWalk at EOF
  277.                 // iso. null.
  278.                 return TreeWalk.forPath(repo, path, beforeTree);
  279.             }
  280.             TreeWalk walk = new TreeWalk(repo);

  281.             // Use a TreeWalk with a DirCacheIterator to pick up the correct
  282.             // clean/smudge filters.
  283.             int cacheTreeIdx = walk.addTree(new DirCacheIterator(cache));
  284.             FileTreeIterator files = new FileTreeIterator(repo);
  285.             if (FILE_TREE_INDEX != walk.addTree(files))
  286.                 throw new IllegalStateException();

  287.             walk.setFilter(AndTreeFilter.create(
  288.                     PathFilterGroup.createFromStrings(path),
  289.                     new NotIgnoredFilter(FILE_TREE_INDEX)));
  290.             walk.setOperationType(OperationType.CHECKIN_OP);
  291.             walk.setRecursive(true);
  292.             files.setDirCacheIterator(walk, cacheTreeIdx);
  293.             return walk;
  294.         } catch (IOException e) {
  295.             throw new PatchApplyException(MessageFormat.format(
  296.                     JGitText.get().patchApplyException, e.getMessage()), e);
  297.         }
  298.     }

  299.     private static final int FILE_TREE_INDEX = 1;

  300.     /**
  301.      * Applies patch to a single file.
  302.      *
  303.      * @param pathWithOriginalContent
  304.      *            The path to use for the pre-image. Also determines CRLF and
  305.      *            smudge settings.
  306.      * @param dirCache
  307.      *            Dircache to read existing data from.
  308.      * @param dirCacheBuilder
  309.      *            Builder for Dircache to write new data to.
  310.      * @param f
  311.      *            The file to update with new contents. Null for inCore usage.
  312.      * @param fh
  313.      *            The patch header.
  314.      * @throws PatchApplyException
  315.      */
  316.     private void apply(String pathWithOriginalContent, DirCache dirCache,
  317.             DirCacheBuilder dirCacheBuilder, @Nullable File f,
  318.             org.eclipse.jgit.patch.FileHeader fh) throws PatchApplyException {
  319.         if (PatchType.BINARY.equals(fh.getPatchType())) {
  320.             // This patch type just says "something changed". We can't do
  321.             // anything with that.
  322.             // Maybe this should return an error code, though?
  323.             return;
  324.         }
  325.         try {
  326.             TreeWalk walk = getTreeWalkForFile(pathWithOriginalContent, dirCache);
  327.             boolean loadedFromTreeWalk = false;
  328.             // CR-LF handling is determined by whether the file or the patch
  329.             // have CR-LF line endings.
  330.             boolean convertCrLf = inCore() || needsCrLfConversion(f, fh);
  331.             EolStreamType streamType = convertCrLf ? EolStreamType.TEXT_CRLF
  332.                     : EolStreamType.DIRECT;
  333.             String smudgeFilterCommand = null;
  334.             StreamSupplier fileStreamSupplier = null;
  335.             ObjectId fileId = ObjectId.zeroId();
  336.             if (walk == null) {
  337.                 // For new files with inCore()==true, TreeWalk.forPath can be
  338.                 // null. Stay with defaults.
  339.             } else if (inCore()) {
  340.                 fileId = walk.getObjectId(0);
  341.                 ObjectLoader loader = LfsFactory.getInstance()
  342.                         .applySmudgeFilter(repo, reader.open(fileId, OBJ_BLOB),
  343.                                 null);
  344.                 byte[] data = loader.getBytes();
  345.                 convertCrLf = RawText.isCrLfText(data);
  346.                 fileStreamSupplier = () -> new ByteArrayInputStream(data);
  347.                 streamType = convertCrLf ? EolStreamType.TEXT_CRLF
  348.                         : EolStreamType.DIRECT;
  349.                 smudgeFilterCommand = walk
  350.                         .getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE);
  351.                 loadedFromTreeWalk = true;
  352.             } else if (walk.next()) {
  353.                 // If the file on disk has no newline characters,
  354.                 // convertCrLf will be false. In that case we want to honor the
  355.                 // normal git settings.
  356.                 streamType = convertCrLf ? EolStreamType.TEXT_CRLF
  357.                         : walk.getEolStreamType(OperationType.CHECKOUT_OP);
  358.                 smudgeFilterCommand = walk
  359.                         .getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE);
  360.                 FileTreeIterator file = walk.getTree(FILE_TREE_INDEX,
  361.                         FileTreeIterator.class);
  362.                 if (file != null) {
  363.                     fileId = file.getEntryObjectId();
  364.                     fileStreamSupplier = file::openEntryStream;
  365.                     loadedFromTreeWalk = true;
  366.                 } else {
  367.                     throw new PatchApplyException(MessageFormat.format(
  368.                             JGitText.get().cannotReadFile,
  369.                             pathWithOriginalContent));
  370.                 }
  371.             }

  372.             if (fileStreamSupplier == null)
  373.                 fileStreamSupplier = inCore() ? InputStream::nullInputStream
  374.                         : () -> new FileInputStream(f);

  375.             FileMode fileMode = fh.getNewMode() != null ? fh.getNewMode()
  376.                     : FileMode.REGULAR_FILE;
  377.             ContentStreamLoader resultStreamLoader;
  378.             if (PatchType.GIT_BINARY.equals(fh.getPatchType())) {
  379.                 // binary patches are processed in a streaming fashion. Some
  380.                 // binary patches do random access on the input data, so we can't
  381.                 // overwrite the file while we're streaming.
  382.                 resultStreamLoader = applyBinary(pathWithOriginalContent, f, fh,
  383.                         fileStreamSupplier, fileId);
  384.             } else {
  385.                 String filterCommand = walk != null
  386.                         ? walk.getFilterCommand(
  387.                                 Constants.ATTR_FILTER_TYPE_CLEAN)
  388.                         : null;
  389.                 RawText raw = getRawText(f, fileStreamSupplier, fileId,
  390.                         pathWithOriginalContent, loadedFromTreeWalk, filterCommand,
  391.                         convertCrLf);
  392.                 resultStreamLoader = applyText(raw, fh);
  393.             }

  394.             if (f != null) {
  395.                 // Write to a buffer and copy to the file only if everything was
  396.                 // fine.
  397.                 TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
  398.                 try {
  399.                     CheckoutMetadata metadata = new CheckoutMetadata(streamType,
  400.                             smudgeFilterCommand);

  401.                     try (TemporaryBuffer buf = buffer) {
  402.                         DirCacheCheckout.getContent(repo, pathWithOriginalContent,
  403.                                 metadata, resultStreamLoader.supplier, workingTreeOptions,
  404.                                 buf);
  405.                     }
  406.                     try (InputStream bufIn = buffer.openInputStream()) {
  407.                         Files.copy(bufIn, f.toPath(),
  408.                                 StandardCopyOption.REPLACE_EXISTING);
  409.                     }
  410.                 } finally {
  411.                     buffer.destroy();
  412.                 }

  413.                 repo.getFS().setExecute(f,
  414.                         fileMode == FileMode.EXECUTABLE_FILE);
  415.             }

  416.             Instant lastModified = f == null ? null
  417.                     : repo.getFS().lastModifiedInstant(f);
  418.             Attributes attributes = walk != null ? walk.getAttributes()
  419.                     : new Attributes();

  420.             DirCacheEntry dce = insertToIndex(
  421.                     resultStreamLoader.supplier.load(),
  422.                     fh.getNewPath().getBytes(StandardCharsets.UTF_8), fileMode,
  423.                     lastModified, resultStreamLoader.length,
  424.                     attributes.get(Constants.ATTR_FILTER));
  425.             dirCacheBuilder.add(dce);
  426.             if (PatchType.GIT_BINARY.equals(fh.getPatchType())
  427.                     && fh.getNewId() != null && fh.getNewId().isComplete()
  428.                     && !fh.getNewId().toObjectId().equals(dce.getObjectId())) {
  429.                 throw new PatchApplyException(MessageFormat.format(
  430.                         JGitText.get().applyBinaryResultOidWrong,
  431.                         pathWithOriginalContent));
  432.             }
  433.         } catch (IOException | UnsupportedOperationException e) {
  434.             throw new PatchApplyException(MessageFormat.format(
  435.                     JGitText.get().patchApplyException, e.getMessage()), e);
  436.         }
  437.     }

  438.     private DirCacheEntry insertToIndex(InputStream input, byte[] path,
  439.             FileMode fileMode, Instant lastModified, long length,
  440.             Attribute lfsAttribute) throws IOException {
  441.         DirCacheEntry dce = new DirCacheEntry(path, DirCacheEntry.STAGE_0);
  442.         dce.setFileMode(fileMode);
  443.         if (lastModified != null) {
  444.             dce.setLastModified(lastModified);
  445.         }
  446.         dce.setLength(length);

  447.         try (LfsInputStream is = org.eclipse.jgit.util.LfsFactory.getInstance()
  448.                 .applyCleanFilter(repo, input, length, lfsAttribute)) {
  449.             dce.setObjectId(inserter.insert(OBJ_BLOB, is.getLength(), is));
  450.         }

  451.         return dce;
  452.     }

  453.     /**
  454.      * Gets the raw text of the given file.
  455.      *
  456.      * @param file
  457.      *            to read from
  458.      * @param fileStreamSupplier
  459.      *            if fromTreewalk, the stream of the file content
  460.      * @param fileId
  461.      *            of the file
  462.      * @param path
  463.      *            of the file
  464.      * @param fromTreeWalk
  465.      *            whether the file was loaded by a {@link TreeWalk}
  466.      * @param filterCommand
  467.      *            for reading the file content
  468.      * @param convertCrLf
  469.      *            whether a CR-LF conversion is needed
  470.      * @return the result raw text
  471.      * @throws IOException
  472.      *             in case of filtering issues
  473.      */
  474.     private RawText getRawText(@Nullable File file,
  475.             StreamSupplier fileStreamSupplier, ObjectId fileId, String path,
  476.             boolean fromTreeWalk, String filterCommand, boolean convertCrLf)
  477.             throws IOException {
  478.         if (fromTreeWalk) {
  479.             // Can't use file.openEntryStream() as we cannot control its CR-LF
  480.             // conversion.
  481.             try (InputStream input = filterClean(repo, path,
  482.                     fileStreamSupplier.load(), convertCrLf, filterCommand)) {
  483.                 return new RawText(org.eclipse.jgit.util.IO
  484.                         .readWholeStream(input, 0).array());
  485.             }
  486.         }
  487.         if (convertCrLf) {
  488.             try (InputStream input = EolStreamTypeUtil.wrapInputStream(
  489.                     fileStreamSupplier.load(), EolStreamType.TEXT_LF)) {
  490.                 return new RawText(org.eclipse.jgit.util.IO
  491.                         .readWholeStream(input, 0).array());
  492.             }
  493.         }
  494.         if (inCore() && fileId.equals(ObjectId.zeroId())) {
  495.             return new RawText(new byte[] {});
  496.         }
  497.         return new RawText(file);
  498.     }

  499.     private InputStream filterClean(Repository repository, String path,
  500.             InputStream fromFile, boolean convertCrLf, String filterCommand)
  501.             throws IOException {
  502.         InputStream input = fromFile;
  503.         if (convertCrLf) {
  504.             input = EolStreamTypeUtil.wrapInputStream(input,
  505.                     EolStreamType.TEXT_LF);
  506.         }
  507.         if (org.eclipse.jgit.util.StringUtils.isEmptyOrNull(filterCommand)) {
  508.             return input;
  509.         }
  510.         if (FilterCommandRegistry.isRegistered(filterCommand)) {
  511.             LocalFile buffer = new org.eclipse.jgit.util.TemporaryBuffer.LocalFile(
  512.                     null, inCoreSizeLimit);
  513.             FilterCommand command = FilterCommandRegistry.createFilterCommand(
  514.                     filterCommand, repository, input, buffer);
  515.             while (command.run() != -1) {
  516.                 // loop as long as command.run() tells there is work to do
  517.             }
  518.             return buffer.openInputStreamWithAutoDestroy();
  519.         }
  520.         org.eclipse.jgit.util.FS fs = repository.getFS();
  521.         ProcessBuilder filterProcessBuilder = fs.runInShell(filterCommand,
  522.                 new String[0]);
  523.         filterProcessBuilder.directory(repository.getWorkTree());
  524.         filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
  525.                 repository.getDirectory().getAbsolutePath());
  526.         ExecutionResult result;
  527.         try {
  528.             result = fs.execute(filterProcessBuilder, input);
  529.         } catch (IOException | InterruptedException e) {
  530.             throw new IOException(
  531.                     new FilterFailedException(e, filterCommand, path));
  532.         }
  533.         int rc = result.getRc();
  534.         if (rc != 0) {
  535.             throw new IOException(new FilterFailedException(rc, filterCommand,
  536.                     path, result.getStdout().toByteArray(4096),
  537.                     org.eclipse.jgit.util.RawParseUtils
  538.                             .decode(result.getStderr().toByteArray(4096))));
  539.         }
  540.         return result.getStdout().openInputStreamWithAutoDestroy();
  541.     }

  542.     private boolean needsCrLfConversion(File f,
  543.             org.eclipse.jgit.patch.FileHeader fileHeader) throws IOException {
  544.         if (PatchType.GIT_BINARY.equals(fileHeader.getPatchType())) {
  545.             return false;
  546.         }
  547.         if (!hasCrLf(fileHeader)) {
  548.             try (InputStream input = new FileInputStream(f)) {
  549.                 return RawText.isCrLfText(input);
  550.             }
  551.         }
  552.         return false;
  553.     }

  554.     private static boolean hasCrLf(
  555.             org.eclipse.jgit.patch.FileHeader fileHeader) {
  556.         if (PatchType.GIT_BINARY.equals(fileHeader.getPatchType())) {
  557.             return false;
  558.         }
  559.         for (org.eclipse.jgit.patch.HunkHeader header : fileHeader.getHunks()) {
  560.             byte[] buf = header.getBuffer();
  561.             int hunkEnd = header.getEndOffset();
  562.             int lineStart = header.getStartOffset();
  563.             while (lineStart < hunkEnd) {
  564.                 int nextLineStart = RawParseUtils.nextLF(buf, lineStart);
  565.                 if (nextLineStart > hunkEnd) {
  566.                     nextLineStart = hunkEnd;
  567.                 }
  568.                 if (nextLineStart <= lineStart) {
  569.                     break;
  570.                 }
  571.                 if (nextLineStart - lineStart > 1) {
  572.                     char first = (char) (buf[lineStart] & 0xFF);
  573.                     if (first == ' ' || first == '-') {
  574.                         // It's an old line. Does it end in CR-LF?
  575.                         if (buf[nextLineStart - 2] == '\r') {
  576.                             return true;
  577.                         }
  578.                     }
  579.                 }
  580.                 lineStart = nextLineStart;
  581.             }
  582.         }
  583.         return false;
  584.     }

  585.     private ObjectId hash(File f) throws IOException {
  586.         try (FileInputStream fis = new FileInputStream(f);
  587.                 SHA1InputStream shaStream = new SHA1InputStream(fis,
  588.                         f.length())) {
  589.             shaStream.transferTo(OutputStream.nullOutputStream());
  590.             return shaStream.getHash().toObjectId();
  591.         }
  592.     }

  593.     private void checkOid(ObjectId baseId, ObjectId id, ChangeType type, File f,
  594.             String path) throws PatchApplyException, IOException {
  595.         boolean hashOk = false;
  596.         if (id != null) {
  597.             hashOk = baseId.equals(id);
  598.             if (!hashOk && ChangeType.ADD.equals(type)
  599.                     && ObjectId.zeroId().equals(baseId)) {
  600.                 // We create a new file. The OID of an empty file is not the
  601.                 // zero id!
  602.                 hashOk = Constants.EMPTY_BLOB_ID.equals(id);
  603.             }
  604.         } else if (!inCore()) {
  605.             if (ObjectId.zeroId().equals(baseId)) {
  606.                 // File empty is OK.
  607.                 hashOk = !f.exists() || f.length() == 0;
  608.             } else {
  609.                 hashOk = baseId.equals(hash(f));
  610.             }
  611.         }
  612.         if (!hashOk) {
  613.             throw new PatchApplyException(MessageFormat
  614.                     .format(JGitText.get().applyBinaryBaseOidWrong, path));
  615.         }
  616.     }

  617.     private boolean inCore() {
  618.         return beforeTree != null;
  619.     }

  620.     /**
  621.      * Provide stream, along with the length of the object. We use this once to
  622.      * patch to the working tree, once to write the index. For on-disk
  623.      * operation, presumably we could stream to the destination file, and then
  624.      * read back the stream from disk. We don't because it is more complex.
  625.      */
  626.     private static class ContentStreamLoader {

  627.         StreamSupplier supplier;

  628.         long length;

  629.         ContentStreamLoader(StreamSupplier supplier, long length) {
  630.             this.supplier = supplier;
  631.             this.length = length;
  632.         }
  633.     }

  634.     /**
  635.      * Applies a binary patch.
  636.      *
  637.      * @param path
  638.      *            pathname of the file to write.
  639.      * @param f
  640.      *            destination file
  641.      * @param fh
  642.      *            the patch to apply
  643.      * @param inputSupplier
  644.      *            a supplier for the contents of the old file
  645.      * @param id
  646.      *            SHA1 for the old content
  647.      * @return a loader for the new content.
  648.      * @throws PatchApplyException
  649.      * @throws IOException
  650.      * @throws UnsupportedOperationException
  651.      */
  652.     private ContentStreamLoader applyBinary(String path, File f,
  653.             org.eclipse.jgit.patch.FileHeader fh, StreamSupplier inputSupplier,
  654.             ObjectId id) throws PatchApplyException, IOException,
  655.             UnsupportedOperationException {
  656.         if (!fh.getOldId().isComplete() || !fh.getNewId().isComplete()) {
  657.             throw new PatchApplyException(MessageFormat
  658.                     .format(JGitText.get().applyBinaryOidTooShort, path));
  659.         }
  660.         org.eclipse.jgit.patch.BinaryHunk hunk = fh.getForwardBinaryHunk();
  661.         // A BinaryHunk has the start at the "literal" or "delta" token. Data
  662.         // starts on the next line.
  663.         int start = RawParseUtils.nextLF(hunk.getBuffer(),
  664.                 hunk.getStartOffset());
  665.         int length = hunk.getEndOffset() - start;
  666.         switch (hunk.getType()) {
  667.         case LITERAL_DEFLATED: {
  668.             // This just overwrites the file. We need to check the hash of
  669.             // the base.
  670.             checkOid(fh.getOldId().toObjectId(), id, fh.getChangeType(), f,
  671.                     path);
  672.             StreamSupplier supp = () -> new InflaterInputStream(
  673.                     new BinaryHunkInputStream(new ByteArrayInputStream(
  674.                             hunk.getBuffer(), start, length)));
  675.             return new ContentStreamLoader(supp, hunk.getSize());
  676.         }
  677.         case DELTA_DEFLATED: {
  678.             // Unfortunately delta application needs random access to the
  679.             // base to construct the result.
  680.             byte[] base;
  681.             try (InputStream in = inputSupplier.load()) {
  682.                 base = IO.readWholeStream(in, 0).array();
  683.             }
  684.             // At least stream the result! We don't have to close these streams,
  685.             // as they don't hold resources.
  686.             StreamSupplier supp = () -> new BinaryDeltaInputStream(base,
  687.                     new InflaterInputStream(
  688.                             new BinaryHunkInputStream(new ByteArrayInputStream(
  689.                                     hunk.getBuffer(), start, length))));

  690.             // This just reads the first bits of the stream.
  691.             long finalSize = ((BinaryDeltaInputStream) supp.load()).getExpectedResultSize();

  692.             return new ContentStreamLoader(supp, finalSize);
  693.         }
  694.         default:
  695.             throw new UnsupportedOperationException(MessageFormat.format(
  696.                     JGitText.get().applyBinaryPatchTypeNotSupported,
  697.                     hunk.getType().name()));
  698.         }
  699.     }

  700.     private ContentStreamLoader applyText(RawText rt,
  701.             org.eclipse.jgit.patch.FileHeader fh)
  702.             throws IOException, PatchApplyException {
  703.         List<ByteBuffer> oldLines = new ArrayList<>(rt.size());
  704.         for (int i = 0; i < rt.size(); i++) {
  705.             oldLines.add(rt.getRawString(i));
  706.         }
  707.         List<ByteBuffer> newLines = new ArrayList<>(oldLines);
  708.         int afterLastHunk = 0;
  709.         int lineNumberShift = 0;
  710.         int lastHunkNewLine = -1;
  711.         for (org.eclipse.jgit.patch.HunkHeader hh : fh.getHunks()) {
  712.             // We assume hunks to be ordered
  713.             if (hh.getNewStartLine() <= lastHunkNewLine) {
  714.                 throw new PatchApplyException(MessageFormat
  715.                         .format(JGitText.get().patchApplyException, hh));
  716.             }
  717.             lastHunkNewLine = hh.getNewStartLine();

  718.             byte[] b = new byte[hh.getEndOffset() - hh.getStartOffset()];
  719.             System.arraycopy(hh.getBuffer(), hh.getStartOffset(), b, 0,
  720.                     b.length);
  721.             RawText hrt = new RawText(b);

  722.             List<ByteBuffer> hunkLines = new ArrayList<>(hrt.size());
  723.             for (int i = 0; i < hrt.size(); i++) {
  724.                 hunkLines.add(hrt.getRawString(i));
  725.             }

  726.             if (hh.getNewStartLine() == 0) {
  727.                 // Must be the single hunk for clearing all content
  728.                 if (fh.getHunks().size() == 1
  729.                         && canApplyAt(hunkLines, newLines, 0)) {
  730.                     newLines.clear();
  731.                     break;
  732.                 }
  733.                 throw new PatchApplyException(MessageFormat
  734.                         .format(JGitText.get().patchApplyException, hh));
  735.             }
  736.             // Hunk lines as reported by the hunk may be off, so don't rely on
  737.             // them.
  738.             int applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
  739.             // But they definitely should not go backwards.
  740.             if (applyAt < afterLastHunk && lineNumberShift < 0) {
  741.                 applyAt = hh.getNewStartLine() - 1;
  742.                 lineNumberShift = 0;
  743.             }
  744.             if (applyAt < afterLastHunk) {
  745.                 throw new PatchApplyException(MessageFormat
  746.                         .format(JGitText.get().patchApplyException, hh));
  747.             }
  748.             boolean applies = false;
  749.             int oldLinesInHunk = hh.getLinesContext()
  750.                     + hh.getOldImage().getLinesDeleted();
  751.             if (oldLinesInHunk <= 1) {
  752.                 // Don't shift hunks without context lines. Just try the
  753.                 // position corrected by the current lineNumberShift, and if
  754.                 // that fails, the position recorded in the hunk header.
  755.                 applies = canApplyAt(hunkLines, newLines, applyAt);
  756.                 if (!applies && lineNumberShift != 0) {
  757.                     applyAt = hh.getNewStartLine() - 1;
  758.                     applies = applyAt >= afterLastHunk
  759.                             && canApplyAt(hunkLines, newLines, applyAt);
  760.                 }
  761.             } else {
  762.                 int maxShift = applyAt - afterLastHunk;
  763.                 for (int shift = 0; shift <= maxShift; shift++) {
  764.                     if (canApplyAt(hunkLines, newLines, applyAt - shift)) {
  765.                         applies = true;
  766.                         applyAt -= shift;
  767.                         break;
  768.                     }
  769.                 }
  770.                 if (!applies) {
  771.                     // Try shifting the hunk downwards
  772.                     applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
  773.                     maxShift = newLines.size() - applyAt - oldLinesInHunk;
  774.                     for (int shift = 1; shift <= maxShift; shift++) {
  775.                         if (canApplyAt(hunkLines, newLines, applyAt + shift)) {
  776.                             applies = true;
  777.                             applyAt += shift;
  778.                             break;
  779.                         }
  780.                     }
  781.                 }
  782.             }
  783.             if (!applies) {
  784.                 throw new PatchApplyException(MessageFormat
  785.                         .format(JGitText.get().patchApplyException, hh));
  786.             }
  787.             // Hunk applies at applyAt. Apply it, and update afterLastHunk and
  788.             // lineNumberShift
  789.             lineNumberShift = applyAt - hh.getNewStartLine() + 1;
  790.             int sz = hunkLines.size();
  791.             for (int j = 1; j < sz; j++) {
  792.                 ByteBuffer hunkLine = hunkLines.get(j);
  793.                 if (!hunkLine.hasRemaining()) {
  794.                     // Completely empty line; accept as empty context line
  795.                     applyAt++;
  796.                     continue;
  797.                 }
  798.                 switch (hunkLine.array()[hunkLine.position()]) {
  799.                 case ' ':
  800.                     applyAt++;
  801.                     break;
  802.                 case '-':
  803.                     newLines.remove(applyAt);
  804.                     break;
  805.                 case '+':
  806.                     newLines.add(applyAt++, slice(hunkLine, 1));
  807.                     break;
  808.                 default:
  809.                     break;
  810.                 }
  811.             }
  812.             afterLastHunk = applyAt;
  813.         }
  814.         if (!isNoNewlineAtEndOfFile(fh)) {
  815.             newLines.add(null);
  816.         }
  817.         if (!rt.isMissingNewlineAtEnd()) {
  818.             oldLines.add(null);
  819.         }

  820.         // We could check if old == new, but the short-circuiting complicates
  821.         // logic for inCore patching, so just write the new thing regardless.
  822.         TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
  823.         try (OutputStream out = buffer) {
  824.             for (Iterator<ByteBuffer> l = newLines.iterator(); l.hasNext();) {
  825.                 ByteBuffer line = l.next();
  826.                 if (line == null) {
  827.                     // Must be the marker for the final newline
  828.                     break;
  829.                 }
  830.                 out.write(line.array(), line.position(), line.remaining());
  831.                 if (l.hasNext()) {
  832.                     out.write('\n');
  833.                 }
  834.             }
  835.             return new ContentStreamLoader(buffer::openInputStream,
  836.                     buffer.length());
  837.         }
  838.     }

  839.     private boolean canApplyAt(List<ByteBuffer> hunkLines,
  840.             List<ByteBuffer> newLines, int line) {
  841.         int sz = hunkLines.size();
  842.         int limit = newLines.size();
  843.         int pos = line;
  844.         for (int j = 1; j < sz; j++) {
  845.             ByteBuffer hunkLine = hunkLines.get(j);
  846.             if (!hunkLine.hasRemaining()) {
  847.                 // Empty line. Accept as empty context line.
  848.                 if (pos >= limit || newLines.get(pos).hasRemaining()) {
  849.                     return false;
  850.                 }
  851.                 pos++;
  852.                 continue;
  853.             }
  854.             switch (hunkLine.array()[hunkLine.position()]) {
  855.             case ' ':
  856.             case '-':
  857.                 if (pos >= limit
  858.                         || !newLines.get(pos).equals(slice(hunkLine, 1))) {
  859.                     return false;
  860.                 }
  861.                 pos++;
  862.                 break;
  863.             default:
  864.                 break;
  865.             }
  866.         }
  867.         return true;
  868.     }

  869.     private ByteBuffer slice(ByteBuffer b, int off) {
  870.         int newOffset = b.position() + off;
  871.         return ByteBuffer.wrap(b.array(), newOffset, b.limit() - newOffset);
  872.     }

  873.     private boolean isNoNewlineAtEndOfFile(
  874.             org.eclipse.jgit.patch.FileHeader fh) {
  875.         List<? extends org.eclipse.jgit.patch.HunkHeader> hunks = fh.getHunks();
  876.         if (hunks == null || hunks.isEmpty()) {
  877.             return false;
  878.         }
  879.         org.eclipse.jgit.patch.HunkHeader lastHunk = hunks
  880.                 .get(hunks.size() - 1);
  881.         byte[] buf = new byte[lastHunk.getEndOffset()
  882.                 - lastHunk.getStartOffset()];
  883.         System.arraycopy(lastHunk.getBuffer(), lastHunk.getStartOffset(), buf,
  884.                 0, buf.length);
  885.         RawText lhrt = new RawText(buf);
  886.         return lhrt.getString(lhrt.size() - 1)
  887.                 .equals("\\ No newline at end of file"); //$NON-NLS-1$
  888.     }

  889.     /**
  890.      * An {@link InputStream} that updates a {@link SHA1} on every byte read.
  891.      */
  892.     private static class SHA1InputStream extends InputStream {

  893.         private final SHA1 hash;

  894.         private final InputStream in;

  895.         SHA1InputStream(InputStream in, long size) {
  896.             hash = SHA1.newInstance();
  897.             hash.update(Constants.encodedTypeString(Constants.OBJ_BLOB));
  898.             hash.update((byte) ' ');
  899.             hash.update(Constants.encodeASCII(size));
  900.             hash.update((byte) 0);
  901.             this.in = in;
  902.         }

  903.         public SHA1 getHash() {
  904.             return hash;
  905.         }

  906.         @Override
  907.         public int read() throws IOException {
  908.             int b = in.read();
  909.             if (b >= 0) {
  910.                 hash.update((byte) b);
  911.             }
  912.             return b;
  913.         }

  914.         @Override
  915.         public int read(byte[] b, int off, int len) throws IOException {
  916.             int n = in.read(b, off, len);
  917.             if (n > 0) {
  918.                 hash.update(b, off, n);
  919.             }
  920.             return n;
  921.         }

  922.         @Override
  923.         public void close() throws IOException {
  924.             in.close();
  925.         }
  926.     }
  927. }