ApplyCommand.java
- /*
- * Copyright (C) 2011, 2020 IBM Corporation and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.api;
- import java.io.File;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.Writer;
- import java.nio.file.Files;
- import java.nio.file.StandardCopyOption;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.Iterator;
- import java.util.List;
- import org.eclipse.jgit.api.errors.GitAPIException;
- import org.eclipse.jgit.api.errors.PatchApplyException;
- import org.eclipse.jgit.api.errors.PatchFormatException;
- import org.eclipse.jgit.diff.DiffEntry.ChangeType;
- import org.eclipse.jgit.diff.RawText;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.lib.FileMode;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.patch.FileHeader;
- import org.eclipse.jgit.patch.HunkHeader;
- import org.eclipse.jgit.patch.Patch;
- import org.eclipse.jgit.util.FileUtils;
- /**
- * Apply a patch to files and/or to the index.
- *
- * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-apply.html"
- * >Git documentation about apply</a>
- * @since 2.0
- */
- public class ApplyCommand extends GitCommand<ApplyResult> {
- private InputStream in;
- /**
- * Constructs the command if the patch is to be applied to the index.
- *
- * @param repo
- */
- ApplyCommand(Repository repo) {
- super(repo);
- }
- /**
- * Set patch
- *
- * @param in
- * the patch to apply
- * @return this instance
- */
- public ApplyCommand setPatch(InputStream in) {
- checkCallable();
- this.in = in;
- return this;
- }
- /**
- * {@inheritDoc}
- * <p>
- * Executes the {@code ApplyCommand} command with all the options and
- * parameters collected by the setter methods (e.g.
- * {@link #setPatch(InputStream)} of this class. Each instance of this class
- * should only be used for one invocation of the command. Don't call this
- * method twice on an instance.
- */
- @Override
- public ApplyResult call() throws GitAPIException, PatchFormatException,
- PatchApplyException {
- checkCallable();
- ApplyResult r = new ApplyResult();
- try {
- final Patch p = new Patch();
- try {
- p.parse(in);
- } finally {
- in.close();
- }
- if (!p.getErrors().isEmpty())
- throw new PatchFormatException(p.getErrors());
- for (FileHeader fh : p.getFiles()) {
- ChangeType type = fh.getChangeType();
- File f = null;
- switch (type) {
- case ADD:
- f = getFile(fh.getNewPath(), true);
- apply(f, fh);
- break;
- case MODIFY:
- f = getFile(fh.getOldPath(), false);
- apply(f, fh);
- break;
- case DELETE:
- f = getFile(fh.getOldPath(), false);
- if (!f.delete())
- throw new PatchApplyException(MessageFormat.format(
- JGitText.get().cannotDeleteFile, f));
- break;
- case RENAME:
- f = getFile(fh.getOldPath(), false);
- File dest = getFile(fh.getNewPath(), false);
- try {
- FileUtils.mkdirs(dest.getParentFile(), true);
- FileUtils.rename(f, dest,
- StandardCopyOption.ATOMIC_MOVE);
- } catch (IOException e) {
- throw new PatchApplyException(MessageFormat.format(
- JGitText.get().renameFileFailed, f, dest), e);
- }
- apply(dest, fh);
- break;
- case COPY:
- f = getFile(fh.getOldPath(), false);
- File target = getFile(fh.getNewPath(), false);
- FileUtils.mkdirs(target.getParentFile(), true);
- Files.copy(f.toPath(), target.toPath());
- apply(target, fh);
- }
- r.addUpdatedFile(f);
- }
- } catch (IOException e) {
- throw new PatchApplyException(MessageFormat.format(
- JGitText.get().patchApplyException, e.getMessage()), e);
- }
- setCallable(false);
- return r;
- }
- private File getFile(String path, boolean create)
- throws PatchApplyException {
- File f = new File(getRepository().getWorkTree(), path);
- if (create)
- try {
- File parent = f.getParentFile();
- FileUtils.mkdirs(parent, true);
- FileUtils.createNewFile(f);
- } catch (IOException e) {
- throw new PatchApplyException(MessageFormat.format(
- JGitText.get().createNewFileFailed, f), e);
- }
- return f;
- }
- /**
- * @param f
- * @param fh
- * @throws IOException
- * @throws PatchApplyException
- */
- private void apply(File f, FileHeader fh)
- throws IOException, PatchApplyException {
- RawText rt = new RawText(f);
- List<String> oldLines = new ArrayList<>(rt.size());
- for (int i = 0; i < rt.size(); i++)
- oldLines.add(rt.getString(i));
- List<String> newLines = new ArrayList<>(oldLines);
- int afterLastHunk = 0;
- int lineNumberShift = 0;
- int lastHunkNewLine = -1;
- for (HunkHeader hh : fh.getHunks()) {
- // We assume hunks to be ordered
- if (hh.getNewStartLine() <= lastHunkNewLine) {
- throw new PatchApplyException(MessageFormat
- .format(JGitText.get().patchApplyException, hh));
- }
- lastHunkNewLine = hh.getNewStartLine();
- byte[] b = new byte[hh.getEndOffset() - hh.getStartOffset()];
- System.arraycopy(hh.getBuffer(), hh.getStartOffset(), b, 0,
- b.length);
- RawText hrt = new RawText(b);
- List<String> hunkLines = new ArrayList<>(hrt.size());
- for (int i = 0; i < hrt.size(); i++) {
- hunkLines.add(hrt.getString(i));
- }
- if (hh.getNewStartLine() == 0) {
- // Must be the single hunk for clearing all content
- if (fh.getHunks().size() == 1
- && canApplyAt(hunkLines, newLines, 0)) {
- newLines.clear();
- break;
- }
- throw new PatchApplyException(MessageFormat
- .format(JGitText.get().patchApplyException, hh));
- }
- // Hunk lines as reported by the hunk may be off, so don't rely on
- // them.
- int applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
- // But they definitely should not go backwards.
- if (applyAt < afterLastHunk && lineNumberShift < 0) {
- applyAt = hh.getNewStartLine() - 1;
- lineNumberShift = 0;
- }
- if (applyAt < afterLastHunk) {
- throw new PatchApplyException(MessageFormat
- .format(JGitText.get().patchApplyException, hh));
- }
- boolean applies = false;
- int oldLinesInHunk = hh.getLinesContext()
- + hh.getOldImage().getLinesDeleted();
- if (oldLinesInHunk <= 1) {
- // Don't shift hunks without context lines. Just try the
- // position corrected by the current lineNumberShift, and if
- // that fails, the position recorded in the hunk header.
- applies = canApplyAt(hunkLines, newLines, applyAt);
- if (!applies && lineNumberShift != 0) {
- applyAt = hh.getNewStartLine() - 1;
- applies = applyAt >= afterLastHunk
- && canApplyAt(hunkLines, newLines, applyAt);
- }
- } else {
- int maxShift = applyAt - afterLastHunk;
- for (int shift = 0; shift <= maxShift; shift++) {
- if (canApplyAt(hunkLines, newLines, applyAt - shift)) {
- applies = true;
- applyAt -= shift;
- break;
- }
- }
- if (!applies) {
- // Try shifting the hunk downwards
- applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
- maxShift = newLines.size() - applyAt - oldLinesInHunk;
- for (int shift = 1; shift <= maxShift; shift++) {
- if (canApplyAt(hunkLines, newLines, applyAt + shift)) {
- applies = true;
- applyAt += shift;
- break;
- }
- }
- }
- }
- if (!applies) {
- throw new PatchApplyException(MessageFormat
- .format(JGitText.get().patchApplyException, hh));
- }
- // Hunk applies at applyAt. Apply it, and update afterLastHunk and
- // lineNumberShift
- lineNumberShift = applyAt - hh.getNewStartLine() + 1;
- int sz = hunkLines.size();
- for (int j = 1; j < sz; j++) {
- String hunkLine = hunkLines.get(j);
- switch (hunkLine.charAt(0)) {
- case ' ':
- applyAt++;
- break;
- case '-':
- newLines.remove(applyAt);
- break;
- case '+':
- newLines.add(applyAt++, hunkLine.substring(1));
- break;
- default:
- break;
- }
- }
- afterLastHunk = applyAt;
- }
- if (!isNoNewlineAtEndOfFile(fh)) {
- newLines.add(""); //$NON-NLS-1$
- }
- if (!rt.isMissingNewlineAtEnd()) {
- oldLines.add(""); //$NON-NLS-1$
- }
- if (!isChanged(oldLines, newLines)) {
- return; // Don't touch the file
- }
- try (Writer fw = Files.newBufferedWriter(f.toPath())) {
- for (Iterator<String> l = newLines.iterator(); l.hasNext();) {
- fw.write(l.next());
- if (l.hasNext()) {
- // Don't bother handling line endings - if it was Windows,
- // the \r is still there!
- fw.write('\n');
- }
- }
- }
- getRepository().getFS().setExecute(f, fh.getNewMode() == FileMode.EXECUTABLE_FILE);
- }
- private boolean canApplyAt(List<String> hunkLines, List<String> newLines,
- int line) {
- int sz = hunkLines.size();
- int limit = newLines.size();
- int pos = line;
- for (int j = 1; j < sz; j++) {
- String hunkLine = hunkLines.get(j);
- switch (hunkLine.charAt(0)) {
- case ' ':
- case '-':
- if (pos >= limit
- || !newLines.get(pos).equals(hunkLine.substring(1))) {
- return false;
- }
- pos++;
- break;
- default:
- break;
- }
- }
- return true;
- }
- private static boolean isChanged(List<String> ol, List<String> nl) {
- if (ol.size() != nl.size())
- return true;
- for (int i = 0; i < ol.size(); i++)
- if (!ol.get(i).equals(nl.get(i)))
- return true;
- return false;
- }
- private boolean isNoNewlineAtEndOfFile(FileHeader fh) {
- List<? extends HunkHeader> hunks = fh.getHunks();
- if (hunks == null || hunks.isEmpty()) {
- return false;
- }
- HunkHeader lastHunk = hunks.get(hunks.size() - 1);
- RawText lhrt = new RawText(lastHunk.getBuffer());
- return lhrt.getString(lhrt.size() - 1)
- .equals("\\ No newline at end of file"); //$NON-NLS-1$
- }
- }