123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792 |
- /*
- * Copyright (C) 2011, 2021 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.BufferedInputStream;
- import java.io.ByteArrayInputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.nio.ByteBuffer;
- 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 java.util.zip.InflaterInputStream;
-
- import org.eclipse.jgit.api.errors.FilterFailedException;
- 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.attributes.FilterCommand;
- import org.eclipse.jgit.attributes.FilterCommandRegistry;
- import org.eclipse.jgit.diff.DiffEntry.ChangeType;
- import org.eclipse.jgit.diff.RawText;
- import org.eclipse.jgit.dircache.DirCache;
- import org.eclipse.jgit.dircache.DirCacheCheckout;
- import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
- import org.eclipse.jgit.dircache.DirCacheIterator;
- import org.eclipse.jgit.errors.LargeObjectException;
- import org.eclipse.jgit.errors.MissingObjectException;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.lib.Constants;
- import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
- import org.eclipse.jgit.lib.FileMode;
- import org.eclipse.jgit.lib.ObjectId;
- import org.eclipse.jgit.lib.ObjectLoader;
- import org.eclipse.jgit.lib.ObjectStream;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.patch.BinaryHunk;
- import org.eclipse.jgit.patch.FileHeader;
- import org.eclipse.jgit.patch.FileHeader.PatchType;
- import org.eclipse.jgit.patch.HunkHeader;
- import org.eclipse.jgit.patch.Patch;
- import org.eclipse.jgit.treewalk.FileTreeIterator;
- import org.eclipse.jgit.treewalk.TreeWalk;
- import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
- import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
- import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
- import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
- import org.eclipse.jgit.util.FS;
- import org.eclipse.jgit.util.FS.ExecutionResult;
- import org.eclipse.jgit.util.FileUtils;
- import org.eclipse.jgit.util.IO;
- import org.eclipse.jgit.util.RawParseUtils;
- import org.eclipse.jgit.util.StringUtils;
- 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.EolStreamTypeUtil;
- import org.eclipse.jgit.util.sha1.SHA1;
-
- /**
- * 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.
- *
- * @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();
- setCallable(false);
- 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());
- }
- Repository repository = getRepository();
- DirCache cache = repository.readDirCache();
- for (FileHeader fh : p.getFiles()) {
- ChangeType type = fh.getChangeType();
- File f = null;
- switch (type) {
- case ADD:
- f = getFile(fh.getNewPath(), true);
- apply(repository, fh.getNewPath(), cache, f, fh);
- break;
- case MODIFY:
- f = getFile(fh.getOldPath(), false);
- apply(repository, fh.getOldPath(), cache, 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(repository, fh.getOldPath(), cache, 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(repository, fh.getOldPath(), cache, target, fh);
- }
- r.addUpdatedFile(f);
- }
- } catch (IOException e) {
- throw new PatchApplyException(MessageFormat.format(
- JGitText.get().patchApplyException, e.getMessage()), e);
- }
- 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;
- }
-
- private void apply(Repository repository, String path, DirCache cache,
- File f, FileHeader fh) throws IOException, PatchApplyException {
- if (PatchType.BINARY.equals(fh.getPatchType())) {
- return;
- }
- boolean convertCrLf = needsCrLfConversion(f, fh);
- // Use a TreeWalk with a DirCacheIterator to pick up the correct
- // clean/smudge filters. CR-LF handling is completely determined by
- // whether the file or the patch have CR-LF line endings.
- try (TreeWalk walk = new TreeWalk(repository)) {
- walk.setOperationType(OperationType.CHECKIN_OP);
- FileTreeIterator files = new FileTreeIterator(repository);
- int fileIdx = walk.addTree(files);
- int cacheIdx = walk.addTree(new DirCacheIterator(cache));
- files.setDirCacheIterator(walk, cacheIdx);
- walk.setFilter(AndTreeFilter.create(
- PathFilterGroup.createFromStrings(path),
- new NotIgnoredFilter(fileIdx)));
- walk.setRecursive(true);
- if (walk.next()) {
- // If the file on disk has no newline characters, convertCrLf
- // will be false. In that case we want to honor the normal git
- // settings.
- EolStreamType streamType = convertCrLf ? EolStreamType.TEXT_CRLF
- : walk.getEolStreamType(OperationType.CHECKOUT_OP);
- String command = walk.getFilterCommand(
- Constants.ATTR_FILTER_TYPE_SMUDGE);
- CheckoutMetadata checkOut = new CheckoutMetadata(streamType, command);
- FileTreeIterator file = walk.getTree(fileIdx,
- FileTreeIterator.class);
- if (file != null) {
- if (PatchType.GIT_BINARY.equals(fh.getPatchType())) {
- applyBinary(repository, path, f, fh,
- file::openEntryStream, file.getEntryObjectId(),
- checkOut);
- } else {
- command = walk.getFilterCommand(
- Constants.ATTR_FILTER_TYPE_CLEAN);
- RawText raw;
- // Can't use file.openEntryStream() as it would do CR-LF
- // conversion as usual, not as wanted by us.
- try (InputStream input = filterClean(repository, path,
- new FileInputStream(f), convertCrLf, command)) {
- raw = new RawText(
- IO.readWholeStream(input, 0).array());
- }
- applyText(repository, path, raw, f, fh, checkOut);
- }
- return;
- }
- }
- }
- // File ignored?
- RawText raw;
- CheckoutMetadata checkOut;
- if (PatchType.GIT_BINARY.equals(fh.getPatchType())) {
- checkOut = new CheckoutMetadata(EolStreamType.DIRECT, null);
- applyBinary(repository, path, f, fh, () -> new FileInputStream(f),
- null, checkOut);
- } else {
- if (convertCrLf) {
- try (InputStream input = EolStreamTypeUtil.wrapInputStream(
- new FileInputStream(f), EolStreamType.TEXT_LF)) {
- raw = new RawText(IO.readWholeStream(input, 0).array());
- }
- checkOut = new CheckoutMetadata(EolStreamType.TEXT_CRLF, null);
- } else {
- raw = new RawText(f);
- checkOut = new CheckoutMetadata(EolStreamType.DIRECT, null);
- }
- applyText(repository, path, raw, f, fh, checkOut);
- }
- }
-
- private boolean needsCrLfConversion(File f, FileHeader fileHeader)
- throws IOException {
- if (PatchType.GIT_BINARY.equals(fileHeader.getPatchType())) {
- return false;
- }
- if (!hasCrLf(fileHeader)) {
- try (InputStream input = new FileInputStream(f)) {
- return RawText.isCrLfText(input);
- }
- }
- return false;
- }
-
- private static boolean hasCrLf(FileHeader fileHeader) {
- if (PatchType.GIT_BINARY.equals(fileHeader.getPatchType())) {
- return false;
- }
- for (HunkHeader header : fileHeader.getHunks()) {
- byte[] buf = header.getBuffer();
- int hunkEnd = header.getEndOffset();
- int lineStart = header.getStartOffset();
- while (lineStart < hunkEnd) {
- int nextLineStart = RawParseUtils.nextLF(buf, lineStart);
- if (nextLineStart > hunkEnd) {
- nextLineStart = hunkEnd;
- }
- if (nextLineStart <= lineStart) {
- break;
- }
- if (nextLineStart - lineStart > 1) {
- char first = (char) (buf[lineStart] & 0xFF);
- if (first == ' ' || first == '-') {
- // It's an old line. Does it end in CR-LF?
- if (buf[nextLineStart - 2] == '\r') {
- return true;
- }
- }
- }
- lineStart = nextLineStart;
- }
- }
- return false;
- }
-
- private InputStream filterClean(Repository repository, String path,
- InputStream fromFile, boolean convertCrLf, String filterCommand)
- throws IOException {
- InputStream input = fromFile;
- if (convertCrLf) {
- input = EolStreamTypeUtil.wrapInputStream(input,
- EolStreamType.TEXT_LF);
- }
- if (StringUtils.isEmptyOrNull(filterCommand)) {
- return input;
- }
- if (FilterCommandRegistry.isRegistered(filterCommand)) {
- LocalFile buffer = new TemporaryBuffer.LocalFile(null);
- FilterCommand command = FilterCommandRegistry.createFilterCommand(
- filterCommand, repository, input, buffer);
- while (command.run() != -1) {
- // loop as long as command.run() tells there is work to do
- }
- return buffer.openInputStreamWithAutoDestroy();
- }
- FS fs = repository.getFS();
- ProcessBuilder filterProcessBuilder = fs.runInShell(filterCommand,
- new String[0]);
- filterProcessBuilder.directory(repository.getWorkTree());
- filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
- repository.getDirectory().getAbsolutePath());
- ExecutionResult result;
- try {
- result = fs.execute(filterProcessBuilder, in);
- } catch (IOException | InterruptedException e) {
- throw new IOException(
- new FilterFailedException(e, filterCommand, path));
- }
- int rc = result.getRc();
- if (rc != 0) {
- throw new IOException(new FilterFailedException(rc, filterCommand,
- path, result.getStdout().toByteArray(4096), RawParseUtils
- .decode(result.getStderr().toByteArray(4096))));
- }
- return result.getStdout().openInputStreamWithAutoDestroy();
- }
-
- /**
- * Something that can supply an {@link InputStream}.
- */
- private interface StreamSupplier {
- InputStream load() throws IOException;
- }
-
- /**
- * We write the patch result to a {@link TemporaryBuffer} and then use
- * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF
- * and smudge filters. DirCacheCheckout needs an ObjectLoader, not a
- * TemporaryBuffer, so this class bridges between the two, making any Stream
- * provided by a {@link StreamSupplier} look like an ordinary git blob to
- * DirCacheCheckout.
- */
- private static class StreamLoader extends ObjectLoader {
-
- private StreamSupplier data;
-
- private long size;
-
- StreamLoader(StreamSupplier data, long length) {
- this.data = data;
- this.size = length;
- }
-
- @Override
- public int getType() {
- return Constants.OBJ_BLOB;
- }
-
- @Override
- public long getSize() {
- return size;
- }
-
- @Override
- public boolean isLarge() {
- return true;
- }
-
- @Override
- public byte[] getCachedBytes() throws LargeObjectException {
- throw new LargeObjectException();
- }
-
- @Override
- public ObjectStream openStream()
- throws MissingObjectException, IOException {
- return new ObjectStream.Filter(getType(), getSize(),
- new BufferedInputStream(data.load()));
- }
- }
-
- private void initHash(SHA1 hash, long size) {
- hash.update(Constants.encodedTypeString(Constants.OBJ_BLOB));
- hash.update((byte) ' ');
- hash.update(Constants.encodeASCII(size));
- hash.update((byte) 0);
- }
-
- private ObjectId hash(File f) throws IOException {
- SHA1 hash = SHA1.newInstance();
- initHash(hash, f.length());
- try (InputStream input = new FileInputStream(f)) {
- byte[] buf = new byte[8192];
- int n;
- while ((n = input.read(buf)) >= 0) {
- hash.update(buf, 0, n);
- }
- }
- return hash.toObjectId();
- }
-
- private void checkOid(ObjectId baseId, ObjectId id, ChangeType type, File f,
- String path)
- throws PatchApplyException, IOException {
- boolean hashOk = false;
- if (id != null) {
- hashOk = baseId.equals(id);
- if (!hashOk && ChangeType.ADD.equals(type)
- && ObjectId.zeroId().equals(baseId)) {
- // We create the file first. The OID of an empty file is not the
- // zero id!
- hashOk = Constants.EMPTY_BLOB_ID.equals(id);
- }
- } else {
- if (ObjectId.zeroId().equals(baseId)) {
- // File empty is OK.
- hashOk = !f.exists() || f.length() == 0;
- } else {
- hashOk = baseId.equals(hash(f));
- }
- }
- if (!hashOk) {
- throw new PatchApplyException(MessageFormat
- .format(JGitText.get().applyBinaryBaseOidWrong, path));
- }
- }
-
- private void applyBinary(Repository repository, String path, File f,
- FileHeader fh, StreamSupplier loader, ObjectId id,
- CheckoutMetadata checkOut)
- throws PatchApplyException, IOException {
- if (!fh.getOldId().isComplete() || !fh.getNewId().isComplete()) {
- throw new PatchApplyException(MessageFormat
- .format(JGitText.get().applyBinaryOidTooShort, path));
- }
- BinaryHunk hunk = fh.getForwardBinaryHunk();
- // A BinaryHunk has the start at the "literal" or "delta" token. Data
- // starts on the next line.
- int start = RawParseUtils.nextLF(hunk.getBuffer(),
- hunk.getStartOffset());
- int length = hunk.getEndOffset() - start;
- SHA1 hash = SHA1.newInstance();
- // Write to a buffer and copy to the file only if everything was fine
- TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
- try {
- switch (hunk.getType()) {
- case LITERAL_DEFLATED:
- // This just overwrites the file. We need to check the hash of
- // the base.
- checkOid(fh.getOldId().toObjectId(), id, fh.getChangeType(), f,
- path);
- initHash(hash, hunk.getSize());
- try (OutputStream out = buffer;
- InputStream inflated = new SHA1InputStream(hash,
- new InflaterInputStream(
- new BinaryHunkInputStream(
- new ByteArrayInputStream(
- hunk.getBuffer(), start,
- length))))) {
- DirCacheCheckout.getContent(repository, path, checkOut,
- new StreamLoader(() -> inflated, hunk.getSize()),
- null, out);
- if (!fh.getNewId().toObjectId().equals(hash.toObjectId())) {
- throw new PatchApplyException(MessageFormat.format(
- JGitText.get().applyBinaryResultOidWrong,
- path));
- }
- }
- try (InputStream bufIn = buffer.openInputStream()) {
- Files.copy(bufIn, f.toPath(),
- StandardCopyOption.REPLACE_EXISTING);
- }
- break;
- case DELTA_DEFLATED:
- // Unfortunately delta application needs random access to the
- // base to construct the result.
- byte[] base;
- try (InputStream input = loader.load()) {
- base = IO.readWholeStream(input, 0).array();
- }
- // At least stream the result!
- try (BinaryDeltaInputStream input = new BinaryDeltaInputStream(
- base,
- new InflaterInputStream(new BinaryHunkInputStream(
- new ByteArrayInputStream(hunk.getBuffer(),
- start, length))))) {
- long finalSize = input.getExpectedResultSize();
- initHash(hash, finalSize);
- try (OutputStream out = buffer;
- SHA1InputStream hashed = new SHA1InputStream(hash,
- input)) {
- DirCacheCheckout.getContent(repository, path, checkOut,
- new StreamLoader(() -> hashed, finalSize), null,
- out);
- if (!fh.getNewId().toObjectId()
- .equals(hash.toObjectId())) {
- throw new PatchApplyException(MessageFormat.format(
- JGitText.get().applyBinaryResultOidWrong,
- path));
- }
- }
- }
- try (InputStream bufIn = buffer.openInputStream()) {
- Files.copy(bufIn, f.toPath(),
- StandardCopyOption.REPLACE_EXISTING);
- }
- break;
- default:
- break;
- }
- } finally {
- buffer.destroy();
- }
- }
-
- private void applyText(Repository repository, String path, RawText rt,
- File f, FileHeader fh, CheckoutMetadata checkOut)
- throws IOException, PatchApplyException {
- List<ByteBuffer> oldLines = new ArrayList<>(rt.size());
- for (int i = 0; i < rt.size(); i++) {
- oldLines.add(rt.getRawString(i));
- }
- List<ByteBuffer> 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<ByteBuffer> hunkLines = new ArrayList<>(hrt.size());
- for (int i = 0; i < hrt.size(); i++) {
- hunkLines.add(hrt.getRawString(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++) {
- ByteBuffer hunkLine = hunkLines.get(j);
- if (!hunkLine.hasRemaining()) {
- // Completely empty line; accept as empty context line
- applyAt++;
- continue;
- }
- switch (hunkLine.array()[hunkLine.position()]) {
- case ' ':
- applyAt++;
- break;
- case '-':
- newLines.remove(applyAt);
- break;
- case '+':
- newLines.add(applyAt++, slice(hunkLine, 1));
- break;
- default:
- break;
- }
- }
- afterLastHunk = applyAt;
- }
- if (!isNoNewlineAtEndOfFile(fh)) {
- newLines.add(null);
- }
- if (!rt.isMissingNewlineAtEnd()) {
- oldLines.add(null);
- }
- if (oldLines.equals(newLines)) {
- return; // Unchanged; don't touch the file
- }
-
- TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
- try {
- try (OutputStream out = buffer) {
- for (Iterator<ByteBuffer> l = newLines.iterator(); l
- .hasNext();) {
- ByteBuffer line = l.next();
- if (line == null) {
- // Must be the marker for the final newline
- break;
- }
- out.write(line.array(), line.position(), line.remaining());
- if (l.hasNext()) {
- out.write('\n');
- }
- }
- }
- try (OutputStream output = new FileOutputStream(f)) {
- DirCacheCheckout.getContent(repository, path, checkOut,
- new StreamLoader(buffer::openInputStream,
- buffer.length()),
- null, output);
- }
- } finally {
- buffer.destroy();
- }
- repository.getFS().setExecute(f,
- fh.getNewMode() == FileMode.EXECUTABLE_FILE);
- }
-
- private boolean canApplyAt(List<ByteBuffer> hunkLines,
- List<ByteBuffer> newLines, int line) {
- int sz = hunkLines.size();
- int limit = newLines.size();
- int pos = line;
- for (int j = 1; j < sz; j++) {
- ByteBuffer hunkLine = hunkLines.get(j);
- if (!hunkLine.hasRemaining()) {
- // Empty line. Accept as empty context line.
- if (pos >= limit || newLines.get(pos).hasRemaining()) {
- return false;
- }
- pos++;
- continue;
- }
- switch (hunkLine.array()[hunkLine.position()]) {
- case ' ':
- case '-':
- if (pos >= limit
- || !newLines.get(pos).equals(slice(hunkLine, 1))) {
- return false;
- }
- pos++;
- break;
- default:
- break;
- }
- }
- return true;
- }
-
- private ByteBuffer slice(ByteBuffer b, int off) {
- int newOffset = b.position() + off;
- return ByteBuffer.wrap(b.array(), newOffset, b.limit() - newOffset);
- }
-
- 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);
- byte[] buf = new byte[lastHunk.getEndOffset()
- - lastHunk.getStartOffset()];
- System.arraycopy(lastHunk.getBuffer(), lastHunk.getStartOffset(), buf,
- 0, buf.length);
- RawText lhrt = new RawText(buf);
- return lhrt.getString(lhrt.size() - 1)
- .equals("\\ No newline at end of file"); //$NON-NLS-1$
- }
-
- /**
- * An {@link InputStream} that updates a {@link SHA1} on every byte read.
- * The hash is supposed to have been initialized before reading starts.
- */
- private static class SHA1InputStream extends InputStream {
-
- private final SHA1 hash;
-
- private final InputStream in;
-
- SHA1InputStream(SHA1 hash, InputStream in) {
- this.hash = hash;
- this.in = in;
- }
-
- @Override
- public int read() throws IOException {
- int b = in.read();
- if (b >= 0) {
- hash.update((byte) b);
- }
- return b;
- }
-
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- int n = in.read(b, off, len);
- if (n > 0) {
- hash.update(b, off, n);
- }
- return n;
- }
-
- @Override
- public void close() throws IOException {
- in.close();
- }
- }
- }
|