diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java | 245 |
1 files changed, 192 insertions, 53 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java index da698d6bf6..23e09b9479 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/PatchApplier.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022, Google Inc. and others + * Copyright (C) 2023, Google Inc. 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 @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; @@ -33,12 +34,13 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.zip.InflaterInputStream; + import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.errors.FilterFailedException; -import org.eclipse.jgit.api.errors.PatchFormatException; import org.eclipse.jgit.attributes.Attribute; import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.attributes.FilterCommand; @@ -52,6 +54,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheCheckout.StreamSupplier; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IndexWriteException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Config; @@ -59,6 +62,7 @@ import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.FileModeCache; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectLoader; @@ -81,10 +85,12 @@ import org.eclipse.jgit.util.LfsFactory; import org.eclipse.jgit.util.LfsFactory.LfsInputStream; import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.LocalFile; import org.eclipse.jgit.util.io.BinaryDeltaInputStream; import org.eclipse.jgit.util.io.BinaryHunkInputStream; +import org.eclipse.jgit.util.io.CountingOutputStream; import org.eclipse.jgit.util.io.EolStreamTypeUtil; import org.eclipse.jgit.util.sha1.SHA1; @@ -97,11 +103,12 @@ import org.eclipse.jgit.util.sha1.SHA1; * @since 6.4 */ public class PatchApplier { - private static final byte[] NO_EOL = "\\ No newline at end of file" //$NON-NLS-1$ .getBytes(StandardCharsets.US_ASCII); - /** The tree before applying the patch. Only non-null for inCore operation. */ + /** + * The tree before applying the patch. Only non-null for inCore operation. + */ @Nullable private final RevTree beforeTree; @@ -111,10 +118,14 @@ public class PatchApplier { private final ObjectReader reader; + private final Charset charset; + private WorkingTreeOptions workingTreeOptions; private int inCoreSizeLimit; + private boolean allowConflicts; + /** * @param repo * repository to apply the patch in @@ -124,7 +135,8 @@ public class PatchApplier { inserter = repo.newObjectInserter(); reader = inserter.newReader(); beforeTree = null; - + allowConflicts = false; + charset = StandardCharsets.UTF_8; Config config = repo.getConfig(); workingTreeOptions = config.get(WorkingTreeOptions.KEY); inCoreSizeLimit = config.getInt(ConfigConstants.CONFIG_MERGE_SECTION, @@ -139,11 +151,14 @@ public class PatchApplier { * @param oi * to be used for modifying objects */ - public PatchApplier(Repository repo, RevTree beforeTree, ObjectInserter oi) { + public PatchApplier(Repository repo, RevTree beforeTree, + ObjectInserter oi) { this.repo = repo; this.beforeTree = beforeTree; inserter = oi; reader = oi.newReader(); + allowConflicts = false; + charset = StandardCharsets.UTF_8; } /** @@ -153,35 +168,76 @@ public class PatchApplier { * @since 6.3 */ public static class Result { - /** * A wrapper for a patch applying error that affects a given file. * * @since 6.6 */ + // TODO(ms): rename this class in next major release + @SuppressWarnings("JavaLangClash") public static class Error { + final String msg; - private String msg; - private String oldFileName; - private @Nullable HunkHeader hh; + final String oldFileName; - private Error(String msg, String oldFileName, - @Nullable HunkHeader hh) { + @Nullable + final HunkHeader hh; + + final boolean isGitConflict; + + Error(String msg, String oldFileName, @Nullable HunkHeader hh, + boolean isGitConflict) { this.msg = msg; this.oldFileName = oldFileName; this.hh = hh; + this.isGitConflict = isGitConflict; + } + + /** + * Signals if as part of encountering this error, conflict markers + * were added to the file. + * + * @return {@code true} if conflict markers were added for this + * error. + * + * @since 6.10 + */ + public boolean isGitConflict() { + return isGitConflict; } @Override public String toString() { if (hh != null) { - return MessageFormat.format(JGitText.get().patchApplyErrorWithHunk, - oldFileName, hh, msg); + return MessageFormat.format( + JGitText.get().patchApplyErrorWithHunk, oldFileName, + hh, msg); } - return MessageFormat.format(JGitText.get().patchApplyErrorWithoutHunk, - oldFileName, msg); + return MessageFormat.format( + JGitText.get().patchApplyErrorWithoutHunk, oldFileName, + msg); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof Error)) { + return false; + } + Error error = (Error) o; + return Objects.equals(msg, error.msg) + && Objects.equals(oldFileName, error.oldFileName) + && Objects.equals(hh, error.hh) + && isGitConflict == error.isGitConflict; + } + + @Override + public int hashCode() { + return Objects.hash(msg, oldFileName, hh, + Boolean.valueOf(isGitConflict)); + } } private ObjectId treeId; @@ -191,6 +247,8 @@ public class PatchApplier { private List<Error> errors = new ArrayList<>(); /** + * Get modified paths + * * @return List of modified paths. */ public List<String> getPaths() { @@ -198,6 +256,8 @@ public class PatchApplier { } /** + * Get tree ID + * * @return The applied tree ID. */ public ObjectId getTreeId() { @@ -205,6 +265,8 @@ public class PatchApplier { } /** + * Get errors + * * @return Errors occurred while applying the patch. * * @since 6.6 @@ -213,35 +275,15 @@ public class PatchApplier { return errors; } - private void addError(String msg,String oldFileName, @Nullable HunkHeader hh) { - errors.add(new Error(msg, oldFileName, hh)); + private void addError(String msg, String oldFileName, + @Nullable HunkHeader hh) { + errors.add(new Error(msg, oldFileName, hh, false)); } - } - /** - * Applies the given patch - * - * @param patchInput - * the patch to apply. - * @return the result of the patch - * @throws PatchFormatException - * if the patch cannot be parsed - * @throws IOException - * if the patch read fails - * @deprecated use {@link #applyPatch(Patch)} instead - */ - @Deprecated - public Result applyPatch(InputStream patchInput) - throws PatchFormatException, IOException { - Patch p = new Patch(); - try (InputStream inStream = patchInput) { - p.parse(inStream); - - if (!p.getErrors().isEmpty()) { - throw new PatchFormatException(p.getErrors()); - } + private void addErrorWithGitConflict(String msg, String oldFileName, + @Nullable HunkHeader hh) { + errors.add(new Error(msg, oldFileName, hh, true)); } - return applyPatch(p); } /** @@ -251,6 +293,7 @@ public class PatchApplier { * the patch to apply. * @return the result of the patch * @throws IOException + * if an IO error occurred * @since 6.6 */ public Result applyPatch(Patch p) throws IOException { @@ -258,6 +301,7 @@ public class PatchApplier { DirCache dirCache = inCore() ? DirCache.read(reader, beforeTree) : repo.lockDirCache(); + FileModeCache directoryCache = new FileModeCache(repo); DirCacheBuilder dirCacheBuilder = dirCache.builder(); Set<String> modifiedPaths = new HashSet<>(); for (FileHeader fh : p.getFiles()) { @@ -270,7 +314,8 @@ public class PatchApplier { switch (type) { case ADD: { if (dest != null) { - FileUtils.mkdirs(dest.getParentFile(), true); + directoryCache.safeCreateParentDirectory(fh.getNewPath(), + dest.getParentFile(), false); FileUtils.createNewFile(dest); } apply(fh.getNewPath(), dirCache, dirCacheBuilder, dest, fh, result); @@ -295,7 +340,8 @@ public class PatchApplier { * apply() will write a fresh stream anyway, which will * overwrite if there were hunks in the patch. */ - FileUtils.mkdirs(dest.getParentFile(), true); + directoryCache.safeCreateParentDirectory(fh.getNewPath(), + dest.getParentFile(), false); FileUtils.rename(src, dest, StandardCopyOption.ATOMIC_MOVE); } @@ -306,7 +352,8 @@ public class PatchApplier { } case COPY: { if (!inCore()) { - FileUtils.mkdirs(dest.getParentFile(), true); + directoryCache.safeCreateParentDirectory(fh.getNewPath(), + dest.getParentFile(), false); Files.copy(src.toPath(), dest.toPath()); } apply(fh.getOldPath(), dirCache, dirCacheBuilder, dest, fh, result); @@ -340,8 +387,19 @@ public class PatchApplier { return result; } + /** + * Sets up the {@link PatchApplier} to apply patches even if they conflict. + * + * @return the {@link PatchApplier} to apply any patches + * @since 6.10 + */ + public PatchApplier allowConflicts() { + allowConflicts = true; + return this; + } + private File getFile(String path) { - return (inCore()) ? null : new File(repo.getWorkTree(), path); + return inCore() ? null : new File(repo.getWorkTree(), path); } /* returns null if the path is not found. */ @@ -401,9 +459,28 @@ public class PatchApplier { fh.getPatchType()), fh.getNewPath(), null); isValid = false; } + if (srcShouldExist && !validGitPath(fh.getOldPath())) { + result.addError(JGitText.get().applyPatchSourceInvalid, + fh.getOldPath(), null); + isValid = false; + } + if (destShouldNotExist && !validGitPath(fh.getNewPath())) { + result.addError(JGitText.get().applyPatchDestInvalid, + fh.getNewPath(), null); + isValid = false; + } return isValid; } + private boolean validGitPath(String path) { + try { + SystemReader.getInstance().checkPath(path); + return true; + } catch (CorruptObjectException e) { + return false; + } + } + private static final int FILE_TREE_INDEX = 1; /** @@ -423,6 +500,7 @@ public class PatchApplier { * @param result * The patch application result. * @throws IOException + * if an IO error occurred */ private void apply(String pathWithOriginalContent, DirCache dirCache, DirCacheBuilder dirCacheBuilder, @Nullable File f, FileHeader fh, Result result) @@ -503,7 +581,9 @@ public class PatchApplier { convertCrLf); resultStreamLoader = applyText(raw, fh, result); } - if (resultStreamLoader == null || !result.getErrors().isEmpty()) { + if (resultStreamLoader == null + || (!result.getErrors().isEmpty() && result.getErrors().stream() + .anyMatch(e -> !e.msg.equals("cannot apply hunk")))) { //$NON-NLS-1$ return; } @@ -777,7 +857,9 @@ public class PatchApplier { * The patch application result * @return a loader for the new content, or null if invalid. * @throws IOException + * if an IO error occurred * @throws UnsupportedOperationException + * if an operation isn't supported */ private @Nullable ContentStreamLoader applyBinary(String path, File f, FileHeader fh, StreamSupplier inputSupplier, ObjectId id, Result result) @@ -832,6 +914,7 @@ public class PatchApplier { } } + @SuppressWarnings("ByteBufferBackingArray") private @Nullable ContentStreamLoader applyText(RawText rt, FileHeader fh, Result result) throws IOException { List<ByteBuffer> oldLines = new ArrayList<>(rt.size()); @@ -922,9 +1005,51 @@ public class PatchApplier { } } if (!applies) { - result.addError(JGitText.get().applyTextPatchCannotApplyHunk, - fh.getOldPath(), hh); - return null; + if (!allowConflicts) { + result.addError( + JGitText.get().applyTextPatchCannotApplyHunk, + fh.getOldPath(), hh); + return null; + } + // Insert conflict markers. This is best-guess because the + // file might have changed completely. But at least we give + // the user a graceful state that they can resolve manually. + // An alternative to this is using the 3-way merger. This + // only works if the pre-image SHA is contained in the repo. + // If that was the case, cherry-picking the original commit + // should be preferred to apply a patch. + result.addErrorWithGitConflict("cannot apply hunk", fh.getOldPath(), hh); //$NON-NLS-1$ + newLines.add(Math.min(applyAt++, newLines.size()), + asBytes("<<<<<<< HEAD")); //$NON-NLS-1$ + applyAt += hh.getOldImage().lineCount; + newLines.add(Math.min(applyAt++, newLines.size()), + asBytes("=======")); //$NON-NLS-1$ + + int sz = hunkLines.size(); + for (int j = 1; j < sz; j++) { + ByteBuffer hunkLine = hunkLines.get(j); + if (!hunkLine.hasRemaining()) { + // Completely empty line; accept as empty context + // line + applyAt++; + lastWasRemoval = false; + continue; + } + switch (hunkLine.array()[hunkLine.position()]) { + case ' ': + case '+': + newLines.add(Math.min(applyAt++, newLines.size()), + slice(hunkLine, 1)); + break; + case '-': + case '\\': + default: + break; + } + } + newLines.add(Math.min(applyAt++, newLines.size()), + asBytes(">>>>>>> PATCH")); //$NON-NLS-1$ + continue; } // Hunk applies at applyAt. Apply it, and update afterLastHunk and // lineNumberShift @@ -971,11 +1096,18 @@ public class PatchApplier { } else if (!rt.isMissingNewlineAtEnd()) { newLines.add(null); } + return toContentStreamLoader(newLines); + } + private static ContentStreamLoader toContentStreamLoader( + List<ByteBuffer> newLines) throws IOException { // We could check if old == new, but the short-circuiting complicates // logic for inCore patching, so just write the new thing regardless. TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null); - try (OutputStream out = buffer) { + // TemporaryBuffer::length reports incorrect length until the buffer + // is closed. To use it as input for ContentStreamLoader below, we + // need a wrapper with a reliable in-progress length. + try (CountingOutputStream out = new CountingOutputStream(buffer)) { for (Iterator<ByteBuffer> l = newLines.iterator(); l.hasNext();) { ByteBuffer line = l.next(); if (line == null) { @@ -988,10 +1120,15 @@ public class PatchApplier { } } return new ContentStreamLoader(buffer::openInputStream, - buffer.length()); + out.getCount()); } } + private ByteBuffer asBytes(String str) { + return ByteBuffer.wrap(str.getBytes(charset)); + } + + @SuppressWarnings("ByteBufferBackingArray") private boolean canApplyAt(List<ByteBuffer> hunkLines, List<ByteBuffer> newLines, int line) { int sz = hunkLines.size(); @@ -1023,11 +1160,13 @@ public class PatchApplier { return true; } + @SuppressWarnings("ByteBufferBackingArray") private ByteBuffer slice(ByteBuffer b, int off) { int newOffset = b.position() + off; return ByteBuffer.wrap(b.array(), newOffset, b.limit() - newOffset); } + @SuppressWarnings("ByteBufferBackingArray") private boolean isNoNewlineAtEnd(ByteBuffer hunkLine) { return Arrays.equals(NO_EOL, 0, NO_EOL.length, hunkLine.array(), hunkLine.position(), hunkLine.limit()); @@ -1078,4 +1217,4 @@ public class PatchApplier { in.close(); } } -} +}
\ No newline at end of file |