/* * 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 Git documentation about apply * @since 2.0 */ public class ApplyCommand extends GitCommand { 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} *

* 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 oldLines = new ArrayList<>(rt.size()); for (int i = 0; i < rt.size(); i++) oldLines.add(rt.getString(i)); List 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 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 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 hunkLines, List 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 ol, List 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 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$ } }