blob: e228e8276a5ee898382c55045a67aa89f9d04c44 [file] [log] [blame]
/*
* 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$
}
}