diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/patch')
6 files changed, 225 insertions, 76 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java index 4ba7cca51e..e29af614a6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedFileHeader.java @@ -40,7 +40,6 @@ public class CombinedFileHeader extends FileHeader { super(b, offset); } - /** {@inheritDoc} */ @Override @SuppressWarnings("unchecked") public List<? extends CombinedHunkHeader> getHunks() { @@ -48,9 +47,6 @@ public class CombinedFileHeader extends FileHeader { } /** - * {@inheritDoc} - * <p> - * * @return number of ancestor revisions mentioned in this diff. */ @Override @@ -60,7 +56,7 @@ public class CombinedFileHeader extends FileHeader { /** * {@inheritDoc} - * <p> + * * @return get the file mode of the first parent. */ @Override @@ -81,7 +77,6 @@ public class CombinedFileHeader extends FileHeader { /** * {@inheritDoc} - * <p> * * @return get the object id of the first parent. */ @@ -101,7 +96,6 @@ public class CombinedFileHeader extends FileHeader { return oldIds[nthParent]; } - /** {@inheritDoc} */ @Override public String getScriptText(Charset ocs, Charset ncs) { final Charset[] cs = new Charset[getParentCount() + 1]; @@ -110,11 +104,6 @@ public class CombinedFileHeader extends FileHeader { return getScriptText(cs); } - /** - * {@inheritDoc} - * <p> - * Convert the patch script for this file into a string. - */ @Override public String getScriptText(Charset[] charsetGuess) { return super.getScriptText(charsetGuess); @@ -156,7 +145,6 @@ public class CombinedFileHeader extends FileHeader { return ptr; } - /** {@inheritDoc} */ @Override protected void parseIndexLine(int ptr, int eol) { // "index $asha1,$bsha1..$csha1" @@ -178,7 +166,6 @@ public class CombinedFileHeader extends FileHeader { oldModes = new FileMode[oldIds.length]; } - /** {@inheritDoc} */ @Override protected void parseNewFileMode(int ptr, int eol) { for (int i = 0; i < oldModes.length; i++) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java index 263b1b9ddc..49cf499865 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/CombinedHunkHeader.java @@ -45,13 +45,11 @@ public class CombinedHunkHeader extends HunkHeader { } } - /** {@inheritDoc} */ @Override public CombinedFileHeader getFileHeader() { return (CombinedFileHeader) super.getFileHeader(); } - /** {@inheritDoc} */ @Override public OldImage getOldImage() { return getOldImage(0); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java index 1e6fb780b2..a47b73dc34 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java @@ -187,6 +187,13 @@ public class FileHeader extends DiffEntry { return getScriptText(new Charset[] { oldCharset, newCharset }); } + /** + * Convert the patch script for this file into a string. + * + * @param charsetGuess + * hint which charset is used + * @return the patch script, as a Unicode string. + */ String getScriptText(Charset[] charsetGuess) { if (getHunks().isEmpty()) { // If we have no hunks then we can safely assume the entire diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java index 5618a71782..8d21b6dabb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FormatError.java @@ -91,7 +91,6 @@ public class FormatError { return RawParseUtils.decode(UTF_8, buf, offset, eol); } - /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java index 4b59fcfc63..9e98f9f272 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java @@ -42,27 +42,47 @@ public class HunkHeader { /** Number of lines added by the post-image not in this file. */ int nAdded; - /** @return first line number the hunk starts on in this file. */ + /** + * Get line number where hunk starts + * + * @return first line number the hunk starts on in this file. + */ public int getStartLine() { return startLine; } - /** @return total number of lines this hunk covers in this file. */ + /** + * Get number of lines this hunk covers + * + * @return total number of lines this hunk covers in this file. + */ public int getLineCount() { return lineCount; } - /** @return number of lines deleted by the post-image from this file. */ + /** + * Get number of lines deleted by the post-image + * + * @return number of lines deleted by the post-image from this file. + */ public int getLinesDeleted() { return nDeleted; } - /** @return number of lines added by the post-image not in this file. */ + /** + * Get number of lines added by the post-image + * + * @return number of lines added by the post-image not in this file. + */ public int getLinesAdded() { return nAdded; } - /** @return object id of the pre-image file. */ + /** + * Get id of the pre-image file + * + * @return object id of the pre-image file. + */ public abstract AbbreviatedObjectId getId(); } @@ -409,7 +429,6 @@ public class HunkHeader { offsets[fileIdx] = end < 0 ? s.length() : end + 1; } - /** {@inheritDoc} */ @SuppressWarnings("nls") @Override public String toString() { 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 |