ApplyCommand.java

  1. /*
  2.  * Copyright (C) 2011, 2020 IBM Corporation 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.api;

  11. import java.io.File;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.io.Writer;
  15. import java.nio.file.Files;
  16. import java.nio.file.StandardCopyOption;
  17. import java.text.MessageFormat;
  18. import java.util.ArrayList;
  19. import java.util.Iterator;
  20. import java.util.List;

  21. import org.eclipse.jgit.api.errors.GitAPIException;
  22. import org.eclipse.jgit.api.errors.PatchApplyException;
  23. import org.eclipse.jgit.api.errors.PatchFormatException;
  24. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  25. import org.eclipse.jgit.diff.RawText;
  26. import org.eclipse.jgit.internal.JGitText;
  27. import org.eclipse.jgit.lib.FileMode;
  28. import org.eclipse.jgit.lib.Repository;
  29. import org.eclipse.jgit.patch.FileHeader;
  30. import org.eclipse.jgit.patch.HunkHeader;
  31. import org.eclipse.jgit.patch.Patch;
  32. import org.eclipse.jgit.util.FileUtils;

  33. /**
  34.  * Apply a patch to files and/or to the index.
  35.  *
  36.  * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-apply.html"
  37.  *      >Git documentation about apply</a>
  38.  * @since 2.0
  39.  */
  40. public class ApplyCommand extends GitCommand<ApplyResult> {

  41.     private InputStream in;

  42.     /**
  43.      * Constructs the command if the patch is to be applied to the index.
  44.      *
  45.      * @param repo
  46.      */
  47.     ApplyCommand(Repository repo) {
  48.         super(repo);
  49.     }

  50.     /**
  51.      * Set patch
  52.      *
  53.      * @param in
  54.      *            the patch to apply
  55.      * @return this instance
  56.      */
  57.     public ApplyCommand setPatch(InputStream in) {
  58.         checkCallable();
  59.         this.in = in;
  60.         return this;
  61.     }

  62.     /**
  63.      * {@inheritDoc}
  64.      * <p>
  65.      * Executes the {@code ApplyCommand} command with all the options and
  66.      * parameters collected by the setter methods (e.g.
  67.      * {@link #setPatch(InputStream)} of this class. Each instance of this class
  68.      * should only be used for one invocation of the command. Don't call this
  69.      * method twice on an instance.
  70.      */
  71.     @Override
  72.     public ApplyResult call() throws GitAPIException, PatchFormatException,
  73.             PatchApplyException {
  74.         checkCallable();
  75.         ApplyResult r = new ApplyResult();
  76.         try {
  77.             final Patch p = new Patch();
  78.             try {
  79.                 p.parse(in);
  80.             } finally {
  81.                 in.close();
  82.             }
  83.             if (!p.getErrors().isEmpty())
  84.                 throw new PatchFormatException(p.getErrors());
  85.             for (FileHeader fh : p.getFiles()) {
  86.                 ChangeType type = fh.getChangeType();
  87.                 File f = null;
  88.                 switch (type) {
  89.                 case ADD:
  90.                     f = getFile(fh.getNewPath(), true);
  91.                     apply(f, fh);
  92.                     break;
  93.                 case MODIFY:
  94.                     f = getFile(fh.getOldPath(), false);
  95.                     apply(f, fh);
  96.                     break;
  97.                 case DELETE:
  98.                     f = getFile(fh.getOldPath(), false);
  99.                     if (!f.delete())
  100.                         throw new PatchApplyException(MessageFormat.format(
  101.                                 JGitText.get().cannotDeleteFile, f));
  102.                     break;
  103.                 case RENAME:
  104.                     f = getFile(fh.getOldPath(), false);
  105.                     File dest = getFile(fh.getNewPath(), false);
  106.                     try {
  107.                         FileUtils.mkdirs(dest.getParentFile(), true);
  108.                         FileUtils.rename(f, dest,
  109.                                 StandardCopyOption.ATOMIC_MOVE);
  110.                     } catch (IOException e) {
  111.                         throw new PatchApplyException(MessageFormat.format(
  112.                                 JGitText.get().renameFileFailed, f, dest), e);
  113.                     }
  114.                     apply(dest, fh);
  115.                     break;
  116.                 case COPY:
  117.                     f = getFile(fh.getOldPath(), false);
  118.                     File target = getFile(fh.getNewPath(), false);
  119.                     FileUtils.mkdirs(target.getParentFile(), true);
  120.                     Files.copy(f.toPath(), target.toPath());
  121.                     apply(target, fh);
  122.                 }
  123.                 r.addUpdatedFile(f);
  124.             }
  125.         } catch (IOException e) {
  126.             throw new PatchApplyException(MessageFormat.format(
  127.                     JGitText.get().patchApplyException, e.getMessage()), e);
  128.         }
  129.         setCallable(false);
  130.         return r;
  131.     }

  132.     private File getFile(String path, boolean create)
  133.             throws PatchApplyException {
  134.         File f = new File(getRepository().getWorkTree(), path);
  135.         if (create)
  136.             try {
  137.                 File parent = f.getParentFile();
  138.                 FileUtils.mkdirs(parent, true);
  139.                 FileUtils.createNewFile(f);
  140.             } catch (IOException e) {
  141.                 throw new PatchApplyException(MessageFormat.format(
  142.                         JGitText.get().createNewFileFailed, f), e);
  143.             }
  144.         return f;
  145.     }

  146.     /**
  147.      * @param f
  148.      * @param fh
  149.      * @throws IOException
  150.      * @throws PatchApplyException
  151.      */
  152.     private void apply(File f, FileHeader fh)
  153.             throws IOException, PatchApplyException {
  154.         RawText rt = new RawText(f);
  155.         List<String> oldLines = new ArrayList<>(rt.size());
  156.         for (int i = 0; i < rt.size(); i++)
  157.             oldLines.add(rt.getString(i));
  158.         List<String> newLines = new ArrayList<>(oldLines);
  159.         int afterLastHunk = 0;
  160.         int lineNumberShift = 0;
  161.         int lastHunkNewLine = -1;
  162.         for (HunkHeader hh : fh.getHunks()) {

  163.             // We assume hunks to be ordered
  164.             if (hh.getNewStartLine() <= lastHunkNewLine) {
  165.                 throw new PatchApplyException(MessageFormat
  166.                         .format(JGitText.get().patchApplyException, hh));
  167.             }
  168.             lastHunkNewLine = hh.getNewStartLine();

  169.             byte[] b = new byte[hh.getEndOffset() - hh.getStartOffset()];
  170.             System.arraycopy(hh.getBuffer(), hh.getStartOffset(), b, 0,
  171.                     b.length);
  172.             RawText hrt = new RawText(b);

  173.             List<String> hunkLines = new ArrayList<>(hrt.size());
  174.             for (int i = 0; i < hrt.size(); i++) {
  175.                 hunkLines.add(hrt.getString(i));
  176.             }

  177.             if (hh.getNewStartLine() == 0) {
  178.                 // Must be the single hunk for clearing all content
  179.                 if (fh.getHunks().size() == 1
  180.                         && canApplyAt(hunkLines, newLines, 0)) {
  181.                     newLines.clear();
  182.                     break;
  183.                 }
  184.                 throw new PatchApplyException(MessageFormat
  185.                         .format(JGitText.get().patchApplyException, hh));
  186.             }
  187.             // Hunk lines as reported by the hunk may be off, so don't rely on
  188.             // them.
  189.             int applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
  190.             // But they definitely should not go backwards.
  191.             if (applyAt < afterLastHunk && lineNumberShift < 0) {
  192.                 applyAt = hh.getNewStartLine() - 1;
  193.                 lineNumberShift = 0;
  194.             }
  195.             if (applyAt < afterLastHunk) {
  196.                 throw new PatchApplyException(MessageFormat
  197.                         .format(JGitText.get().patchApplyException, hh));
  198.             }
  199.             boolean applies = false;
  200.             int oldLinesInHunk = hh.getLinesContext()
  201.                     + hh.getOldImage().getLinesDeleted();
  202.             if (oldLinesInHunk <= 1) {
  203.                 // Don't shift hunks without context lines. Just try the
  204.                 // position corrected by the current lineNumberShift, and if
  205.                 // that fails, the position recorded in the hunk header.
  206.                 applies = canApplyAt(hunkLines, newLines, applyAt);
  207.                 if (!applies && lineNumberShift != 0) {
  208.                     applyAt = hh.getNewStartLine() - 1;
  209.                     applies = applyAt >= afterLastHunk
  210.                             && canApplyAt(hunkLines, newLines, applyAt);
  211.                 }
  212.             } else {
  213.                 int maxShift = applyAt - afterLastHunk;
  214.                 for (int shift = 0; shift <= maxShift; shift++) {
  215.                     if (canApplyAt(hunkLines, newLines, applyAt - shift)) {
  216.                         applies = true;
  217.                         applyAt -= shift;
  218.                         break;
  219.                     }
  220.                 }
  221.                 if (!applies) {
  222.                     // Try shifting the hunk downwards
  223.                     applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
  224.                     maxShift = newLines.size() - applyAt - oldLinesInHunk;
  225.                     for (int shift = 1; shift <= maxShift; shift++) {
  226.                         if (canApplyAt(hunkLines, newLines, applyAt + shift)) {
  227.                             applies = true;
  228.                             applyAt += shift;
  229.                             break;
  230.                         }
  231.                     }
  232.                 }
  233.             }
  234.             if (!applies) {
  235.                 throw new PatchApplyException(MessageFormat
  236.                         .format(JGitText.get().patchApplyException, hh));
  237.             }
  238.             // Hunk applies at applyAt. Apply it, and update afterLastHunk and
  239.             // lineNumberShift
  240.             lineNumberShift = applyAt - hh.getNewStartLine() + 1;
  241.             int sz = hunkLines.size();
  242.             for (int j = 1; j < sz; j++) {
  243.                 String hunkLine = hunkLines.get(j);
  244.                 switch (hunkLine.charAt(0)) {
  245.                 case ' ':
  246.                     applyAt++;
  247.                     break;
  248.                 case '-':
  249.                     newLines.remove(applyAt);
  250.                     break;
  251.                 case '+':
  252.                     newLines.add(applyAt++, hunkLine.substring(1));
  253.                     break;
  254.                 default:
  255.                     break;
  256.                 }
  257.             }
  258.             afterLastHunk = applyAt;
  259.         }
  260.         if (!isNoNewlineAtEndOfFile(fh)) {
  261.             newLines.add(""); //$NON-NLS-1$
  262.         }
  263.         if (!rt.isMissingNewlineAtEnd()) {
  264.             oldLines.add(""); //$NON-NLS-1$
  265.         }
  266.         if (!isChanged(oldLines, newLines)) {
  267.             return; // Don't touch the file
  268.         }
  269.         try (Writer fw = Files.newBufferedWriter(f.toPath())) {
  270.             for (Iterator<String> l = newLines.iterator(); l.hasNext();) {
  271.                 fw.write(l.next());
  272.                 if (l.hasNext()) {
  273.                     // Don't bother handling line endings - if it was Windows,
  274.                     // the \r is still there!
  275.                     fw.write('\n');
  276.                 }
  277.             }
  278.         }
  279.         getRepository().getFS().setExecute(f, fh.getNewMode() == FileMode.EXECUTABLE_FILE);
  280.     }

  281.     private boolean canApplyAt(List<String> hunkLines, List<String> newLines,
  282.             int line) {
  283.         int sz = hunkLines.size();
  284.         int limit = newLines.size();
  285.         int pos = line;
  286.         for (int j = 1; j < sz; j++) {
  287.             String hunkLine = hunkLines.get(j);
  288.             switch (hunkLine.charAt(0)) {
  289.             case ' ':
  290.             case '-':
  291.                 if (pos >= limit
  292.                         || !newLines.get(pos).equals(hunkLine.substring(1))) {
  293.                     return false;
  294.                 }
  295.                 pos++;
  296.                 break;
  297.             default:
  298.                 break;
  299.             }
  300.         }
  301.         return true;
  302.     }

  303.     private static boolean isChanged(List<String> ol, List<String> nl) {
  304.         if (ol.size() != nl.size())
  305.             return true;
  306.         for (int i = 0; i < ol.size(); i++)
  307.             if (!ol.get(i).equals(nl.get(i)))
  308.                 return true;
  309.         return false;
  310.     }

  311.     private boolean isNoNewlineAtEndOfFile(FileHeader fh) {
  312.         List<? extends HunkHeader> hunks = fh.getHunks();
  313.         if (hunks == null || hunks.isEmpty()) {
  314.             return false;
  315.         }
  316.         HunkHeader lastHunk = hunks.get(hunks.size() - 1);
  317.         RawText lhrt = new RawText(lastHunk.getBuffer());
  318.         return lhrt.getString(lhrt.size() - 1)
  319.                 .equals("\\ No newline at end of file"); //$NON-NLS-1$
  320.     }
  321. }