You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ApplyCommand.java 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. /*
  2. * Copyright (C) 2011, 2021 IBM Corporation and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.api;
  11. import java.io.BufferedInputStream;
  12. import java.io.ByteArrayInputStream;
  13. import java.io.File;
  14. import java.io.FileInputStream;
  15. import java.io.FileOutputStream;
  16. import java.io.IOException;
  17. import java.io.InputStream;
  18. import java.io.OutputStream;
  19. import java.nio.ByteBuffer;
  20. import java.nio.file.Files;
  21. import java.nio.file.StandardCopyOption;
  22. import java.text.MessageFormat;
  23. import java.util.ArrayList;
  24. import java.util.Iterator;
  25. import java.util.List;
  26. import java.util.zip.InflaterInputStream;
  27. import org.eclipse.jgit.api.errors.FilterFailedException;
  28. import org.eclipse.jgit.api.errors.GitAPIException;
  29. import org.eclipse.jgit.api.errors.PatchApplyException;
  30. import org.eclipse.jgit.api.errors.PatchFormatException;
  31. import org.eclipse.jgit.attributes.FilterCommand;
  32. import org.eclipse.jgit.attributes.FilterCommandRegistry;
  33. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  34. import org.eclipse.jgit.diff.RawText;
  35. import org.eclipse.jgit.dircache.DirCache;
  36. import org.eclipse.jgit.dircache.DirCacheCheckout;
  37. import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
  38. import org.eclipse.jgit.dircache.DirCacheIterator;
  39. import org.eclipse.jgit.errors.LargeObjectException;
  40. import org.eclipse.jgit.errors.MissingObjectException;
  41. import org.eclipse.jgit.internal.JGitText;
  42. import org.eclipse.jgit.lib.Constants;
  43. import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
  44. import org.eclipse.jgit.lib.FileMode;
  45. import org.eclipse.jgit.lib.ObjectId;
  46. import org.eclipse.jgit.lib.ObjectLoader;
  47. import org.eclipse.jgit.lib.ObjectStream;
  48. import org.eclipse.jgit.lib.Repository;
  49. import org.eclipse.jgit.patch.BinaryHunk;
  50. import org.eclipse.jgit.patch.FileHeader;
  51. import org.eclipse.jgit.patch.FileHeader.PatchType;
  52. import org.eclipse.jgit.patch.HunkHeader;
  53. import org.eclipse.jgit.patch.Patch;
  54. import org.eclipse.jgit.treewalk.FileTreeIterator;
  55. import org.eclipse.jgit.treewalk.TreeWalk;
  56. import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
  57. import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
  58. import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
  59. import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
  60. import org.eclipse.jgit.util.FS;
  61. import org.eclipse.jgit.util.FS.ExecutionResult;
  62. import org.eclipse.jgit.util.FileUtils;
  63. import org.eclipse.jgit.util.IO;
  64. import org.eclipse.jgit.util.RawParseUtils;
  65. import org.eclipse.jgit.util.StringUtils;
  66. import org.eclipse.jgit.util.TemporaryBuffer;
  67. import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
  68. import org.eclipse.jgit.util.io.BinaryDeltaInputStream;
  69. import org.eclipse.jgit.util.io.BinaryHunkInputStream;
  70. import org.eclipse.jgit.util.io.EolStreamTypeUtil;
  71. import org.eclipse.jgit.util.sha1.SHA1;
  72. /**
  73. * Apply a patch to files and/or to the index.
  74. *
  75. * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-apply.html"
  76. * >Git documentation about apply</a>
  77. * @since 2.0
  78. */
  79. public class ApplyCommand extends GitCommand<ApplyResult> {
  80. private InputStream in;
  81. /**
  82. * Constructs the command.
  83. *
  84. * @param repo
  85. */
  86. ApplyCommand(Repository repo) {
  87. super(repo);
  88. }
  89. /**
  90. * Set patch
  91. *
  92. * @param in
  93. * the patch to apply
  94. * @return this instance
  95. */
  96. public ApplyCommand setPatch(InputStream in) {
  97. checkCallable();
  98. this.in = in;
  99. return this;
  100. }
  101. /**
  102. * {@inheritDoc}
  103. * <p>
  104. * Executes the {@code ApplyCommand} command with all the options and
  105. * parameters collected by the setter methods (e.g.
  106. * {@link #setPatch(InputStream)} of this class. Each instance of this class
  107. * should only be used for one invocation of the command. Don't call this
  108. * method twice on an instance.
  109. */
  110. @Override
  111. public ApplyResult call() throws GitAPIException, PatchFormatException,
  112. PatchApplyException {
  113. checkCallable();
  114. setCallable(false);
  115. ApplyResult r = new ApplyResult();
  116. try {
  117. final Patch p = new Patch();
  118. try {
  119. p.parse(in);
  120. } finally {
  121. in.close();
  122. }
  123. if (!p.getErrors().isEmpty()) {
  124. throw new PatchFormatException(p.getErrors());
  125. }
  126. Repository repository = getRepository();
  127. DirCache cache = repository.readDirCache();
  128. for (FileHeader fh : p.getFiles()) {
  129. ChangeType type = fh.getChangeType();
  130. File f = null;
  131. switch (type) {
  132. case ADD:
  133. f = getFile(fh.getNewPath(), true);
  134. apply(repository, fh.getNewPath(), cache, f, fh);
  135. break;
  136. case MODIFY:
  137. f = getFile(fh.getOldPath(), false);
  138. apply(repository, fh.getOldPath(), cache, f, fh);
  139. break;
  140. case DELETE:
  141. f = getFile(fh.getOldPath(), false);
  142. if (!f.delete())
  143. throw new PatchApplyException(MessageFormat.format(
  144. JGitText.get().cannotDeleteFile, f));
  145. break;
  146. case RENAME:
  147. f = getFile(fh.getOldPath(), false);
  148. File dest = getFile(fh.getNewPath(), false);
  149. try {
  150. FileUtils.mkdirs(dest.getParentFile(), true);
  151. FileUtils.rename(f, dest,
  152. StandardCopyOption.ATOMIC_MOVE);
  153. } catch (IOException e) {
  154. throw new PatchApplyException(MessageFormat.format(
  155. JGitText.get().renameFileFailed, f, dest), e);
  156. }
  157. apply(repository, fh.getOldPath(), cache, dest, fh);
  158. break;
  159. case COPY:
  160. f = getFile(fh.getOldPath(), false);
  161. File target = getFile(fh.getNewPath(), false);
  162. FileUtils.mkdirs(target.getParentFile(), true);
  163. Files.copy(f.toPath(), target.toPath());
  164. apply(repository, fh.getOldPath(), cache, target, fh);
  165. }
  166. r.addUpdatedFile(f);
  167. }
  168. } catch (IOException e) {
  169. throw new PatchApplyException(MessageFormat.format(
  170. JGitText.get().patchApplyException, e.getMessage()), e);
  171. }
  172. return r;
  173. }
  174. private File getFile(String path, boolean create)
  175. throws PatchApplyException {
  176. File f = new File(getRepository().getWorkTree(), path);
  177. if (create) {
  178. try {
  179. File parent = f.getParentFile();
  180. FileUtils.mkdirs(parent, true);
  181. FileUtils.createNewFile(f);
  182. } catch (IOException e) {
  183. throw new PatchApplyException(MessageFormat.format(
  184. JGitText.get().createNewFileFailed, f), e);
  185. }
  186. }
  187. return f;
  188. }
  189. private void apply(Repository repository, String path, DirCache cache,
  190. File f, FileHeader fh) throws IOException, PatchApplyException {
  191. if (PatchType.BINARY.equals(fh.getPatchType())) {
  192. return;
  193. }
  194. boolean convertCrLf = needsCrLfConversion(f, fh);
  195. // Use a TreeWalk with a DirCacheIterator to pick up the correct
  196. // clean/smudge filters. CR-LF handling is completely determined by
  197. // whether the file or the patch have CR-LF line endings.
  198. try (TreeWalk walk = new TreeWalk(repository)) {
  199. walk.setOperationType(OperationType.CHECKIN_OP);
  200. FileTreeIterator files = new FileTreeIterator(repository);
  201. int fileIdx = walk.addTree(files);
  202. int cacheIdx = walk.addTree(new DirCacheIterator(cache));
  203. files.setDirCacheIterator(walk, cacheIdx);
  204. walk.setFilter(AndTreeFilter.create(
  205. PathFilterGroup.createFromStrings(path),
  206. new NotIgnoredFilter(fileIdx)));
  207. walk.setRecursive(true);
  208. if (walk.next()) {
  209. // If the file on disk has no newline characters, convertCrLf
  210. // will be false. In that case we want to honor the normal git
  211. // settings.
  212. EolStreamType streamType = convertCrLf ? EolStreamType.TEXT_CRLF
  213. : walk.getEolStreamType(OperationType.CHECKOUT_OP);
  214. String command = walk.getFilterCommand(
  215. Constants.ATTR_FILTER_TYPE_SMUDGE);
  216. CheckoutMetadata checkOut = new CheckoutMetadata(streamType, command);
  217. FileTreeIterator file = walk.getTree(fileIdx,
  218. FileTreeIterator.class);
  219. if (file != null) {
  220. if (PatchType.GIT_BINARY.equals(fh.getPatchType())) {
  221. applyBinary(repository, path, f, fh,
  222. file::openEntryStream, file.getEntryObjectId(),
  223. checkOut);
  224. } else {
  225. command = walk.getFilterCommand(
  226. Constants.ATTR_FILTER_TYPE_CLEAN);
  227. RawText raw;
  228. // Can't use file.openEntryStream() as it would do CR-LF
  229. // conversion as usual, not as wanted by us.
  230. try (InputStream input = filterClean(repository, path,
  231. new FileInputStream(f), convertCrLf, command)) {
  232. raw = new RawText(
  233. IO.readWholeStream(input, 0).array());
  234. }
  235. applyText(repository, path, raw, f, fh, checkOut);
  236. }
  237. return;
  238. }
  239. }
  240. }
  241. // File ignored?
  242. RawText raw;
  243. CheckoutMetadata checkOut;
  244. if (PatchType.GIT_BINARY.equals(fh.getPatchType())) {
  245. checkOut = new CheckoutMetadata(EolStreamType.DIRECT, null);
  246. applyBinary(repository, path, f, fh, () -> new FileInputStream(f),
  247. null, checkOut);
  248. } else {
  249. if (convertCrLf) {
  250. try (InputStream input = EolStreamTypeUtil.wrapInputStream(
  251. new FileInputStream(f), EolStreamType.TEXT_LF)) {
  252. raw = new RawText(IO.readWholeStream(input, 0).array());
  253. }
  254. checkOut = new CheckoutMetadata(EolStreamType.TEXT_CRLF, null);
  255. } else {
  256. raw = new RawText(f);
  257. checkOut = new CheckoutMetadata(EolStreamType.DIRECT, null);
  258. }
  259. applyText(repository, path, raw, f, fh, checkOut);
  260. }
  261. }
  262. private boolean needsCrLfConversion(File f, FileHeader fileHeader)
  263. throws IOException {
  264. if (PatchType.GIT_BINARY.equals(fileHeader.getPatchType())) {
  265. return false;
  266. }
  267. if (!hasCrLf(fileHeader)) {
  268. try (InputStream input = new FileInputStream(f)) {
  269. return RawText.isCrLfText(input);
  270. }
  271. }
  272. return false;
  273. }
  274. private static boolean hasCrLf(FileHeader fileHeader) {
  275. if (PatchType.GIT_BINARY.equals(fileHeader.getPatchType())) {
  276. return false;
  277. }
  278. for (HunkHeader header : fileHeader.getHunks()) {
  279. byte[] buf = header.getBuffer();
  280. int hunkEnd = header.getEndOffset();
  281. int lineStart = header.getStartOffset();
  282. while (lineStart < hunkEnd) {
  283. int nextLineStart = RawParseUtils.nextLF(buf, lineStart);
  284. if (nextLineStart > hunkEnd) {
  285. nextLineStart = hunkEnd;
  286. }
  287. if (nextLineStart <= lineStart) {
  288. break;
  289. }
  290. if (nextLineStart - lineStart > 1) {
  291. char first = (char) (buf[lineStart] & 0xFF);
  292. if (first == ' ' || first == '-') {
  293. // It's an old line. Does it end in CR-LF?
  294. if (buf[nextLineStart - 2] == '\r') {
  295. return true;
  296. }
  297. }
  298. }
  299. lineStart = nextLineStart;
  300. }
  301. }
  302. return false;
  303. }
  304. private InputStream filterClean(Repository repository, String path,
  305. InputStream fromFile, boolean convertCrLf, String filterCommand)
  306. throws IOException {
  307. InputStream input = fromFile;
  308. if (convertCrLf) {
  309. input = EolStreamTypeUtil.wrapInputStream(input,
  310. EolStreamType.TEXT_LF);
  311. }
  312. if (StringUtils.isEmptyOrNull(filterCommand)) {
  313. return input;
  314. }
  315. if (FilterCommandRegistry.isRegistered(filterCommand)) {
  316. LocalFile buffer = new TemporaryBuffer.LocalFile(null);
  317. FilterCommand command = FilterCommandRegistry.createFilterCommand(
  318. filterCommand, repository, input, buffer);
  319. while (command.run() != -1) {
  320. // loop as long as command.run() tells there is work to do
  321. }
  322. return buffer.openInputStreamWithAutoDestroy();
  323. }
  324. FS fs = repository.getFS();
  325. ProcessBuilder filterProcessBuilder = fs.runInShell(filterCommand,
  326. new String[0]);
  327. filterProcessBuilder.directory(repository.getWorkTree());
  328. filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
  329. repository.getDirectory().getAbsolutePath());
  330. ExecutionResult result;
  331. try {
  332. result = fs.execute(filterProcessBuilder, in);
  333. } catch (IOException | InterruptedException e) {
  334. throw new IOException(
  335. new FilterFailedException(e, filterCommand, path));
  336. }
  337. int rc = result.getRc();
  338. if (rc != 0) {
  339. throw new IOException(new FilterFailedException(rc, filterCommand,
  340. path, result.getStdout().toByteArray(4096), RawParseUtils
  341. .decode(result.getStderr().toByteArray(4096))));
  342. }
  343. return result.getStdout().openInputStreamWithAutoDestroy();
  344. }
  345. /**
  346. * Something that can supply an {@link InputStream}.
  347. */
  348. private interface StreamSupplier {
  349. InputStream load() throws IOException;
  350. }
  351. /**
  352. * We write the patch result to a {@link TemporaryBuffer} and then use
  353. * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF
  354. * and smudge filters. DirCacheCheckout needs an ObjectLoader, not a
  355. * TemporaryBuffer, so this class bridges between the two, making any Stream
  356. * provided by a {@link StreamSupplier} look like an ordinary git blob to
  357. * DirCacheCheckout.
  358. */
  359. private static class StreamLoader extends ObjectLoader {
  360. private StreamSupplier data;
  361. private long size;
  362. StreamLoader(StreamSupplier data, long length) {
  363. this.data = data;
  364. this.size = length;
  365. }
  366. @Override
  367. public int getType() {
  368. return Constants.OBJ_BLOB;
  369. }
  370. @Override
  371. public long getSize() {
  372. return size;
  373. }
  374. @Override
  375. public boolean isLarge() {
  376. return true;
  377. }
  378. @Override
  379. public byte[] getCachedBytes() throws LargeObjectException {
  380. throw new LargeObjectException();
  381. }
  382. @Override
  383. public ObjectStream openStream()
  384. throws MissingObjectException, IOException {
  385. return new ObjectStream.Filter(getType(), getSize(),
  386. new BufferedInputStream(data.load()));
  387. }
  388. }
  389. private void initHash(SHA1 hash, long size) {
  390. hash.update(Constants.encodedTypeString(Constants.OBJ_BLOB));
  391. hash.update((byte) ' ');
  392. hash.update(Constants.encodeASCII(size));
  393. hash.update((byte) 0);
  394. }
  395. private ObjectId hash(File f) throws IOException {
  396. SHA1 hash = SHA1.newInstance();
  397. initHash(hash, f.length());
  398. try (InputStream input = new FileInputStream(f)) {
  399. byte[] buf = new byte[8192];
  400. int n;
  401. while ((n = input.read(buf)) >= 0) {
  402. hash.update(buf, 0, n);
  403. }
  404. }
  405. return hash.toObjectId();
  406. }
  407. private void checkOid(ObjectId baseId, ObjectId id, ChangeType type, File f,
  408. String path)
  409. throws PatchApplyException, IOException {
  410. boolean hashOk = false;
  411. if (id != null) {
  412. hashOk = baseId.equals(id);
  413. if (!hashOk && ChangeType.ADD.equals(type)
  414. && ObjectId.zeroId().equals(baseId)) {
  415. // We create the file first. The OID of an empty file is not the
  416. // zero id!
  417. hashOk = Constants.EMPTY_BLOB_ID.equals(id);
  418. }
  419. } else {
  420. if (ObjectId.zeroId().equals(baseId)) {
  421. // File empty is OK.
  422. hashOk = !f.exists() || f.length() == 0;
  423. } else {
  424. hashOk = baseId.equals(hash(f));
  425. }
  426. }
  427. if (!hashOk) {
  428. throw new PatchApplyException(MessageFormat
  429. .format(JGitText.get().applyBinaryBaseOidWrong, path));
  430. }
  431. }
  432. private void applyBinary(Repository repository, String path, File f,
  433. FileHeader fh, StreamSupplier loader, ObjectId id,
  434. CheckoutMetadata checkOut)
  435. throws PatchApplyException, IOException {
  436. if (!fh.getOldId().isComplete() || !fh.getNewId().isComplete()) {
  437. throw new PatchApplyException(MessageFormat
  438. .format(JGitText.get().applyBinaryOidTooShort, path));
  439. }
  440. BinaryHunk hunk = fh.getForwardBinaryHunk();
  441. // A BinaryHunk has the start at the "literal" or "delta" token. Data
  442. // starts on the next line.
  443. int start = RawParseUtils.nextLF(hunk.getBuffer(),
  444. hunk.getStartOffset());
  445. int length = hunk.getEndOffset() - start;
  446. SHA1 hash = SHA1.newInstance();
  447. // Write to a buffer and copy to the file only if everything was fine
  448. TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
  449. try {
  450. switch (hunk.getType()) {
  451. case LITERAL_DEFLATED:
  452. // This just overwrites the file. We need to check the hash of
  453. // the base.
  454. checkOid(fh.getOldId().toObjectId(), id, fh.getChangeType(), f,
  455. path);
  456. initHash(hash, hunk.getSize());
  457. try (OutputStream out = buffer;
  458. InputStream inflated = new SHA1InputStream(hash,
  459. new InflaterInputStream(
  460. new BinaryHunkInputStream(
  461. new ByteArrayInputStream(
  462. hunk.getBuffer(), start,
  463. length))))) {
  464. DirCacheCheckout.getContent(repository, path, checkOut,
  465. new StreamLoader(() -> inflated, hunk.getSize()),
  466. null, out);
  467. if (!fh.getNewId().toObjectId().equals(hash.toObjectId())) {
  468. throw new PatchApplyException(MessageFormat.format(
  469. JGitText.get().applyBinaryResultOidWrong,
  470. path));
  471. }
  472. }
  473. try (InputStream bufIn = buffer.openInputStream()) {
  474. Files.copy(bufIn, f.toPath(),
  475. StandardCopyOption.REPLACE_EXISTING);
  476. }
  477. break;
  478. case DELTA_DEFLATED:
  479. // Unfortunately delta application needs random access to the
  480. // base to construct the result.
  481. byte[] base;
  482. try (InputStream input = loader.load()) {
  483. base = IO.readWholeStream(input, 0).array();
  484. }
  485. // At least stream the result!
  486. try (BinaryDeltaInputStream input = new BinaryDeltaInputStream(
  487. base,
  488. new InflaterInputStream(new BinaryHunkInputStream(
  489. new ByteArrayInputStream(hunk.getBuffer(),
  490. start, length))))) {
  491. long finalSize = input.getExpectedResultSize();
  492. initHash(hash, finalSize);
  493. try (OutputStream out = buffer;
  494. SHA1InputStream hashed = new SHA1InputStream(hash,
  495. input)) {
  496. DirCacheCheckout.getContent(repository, path, checkOut,
  497. new StreamLoader(() -> hashed, finalSize), null,
  498. out);
  499. if (!fh.getNewId().toObjectId()
  500. .equals(hash.toObjectId())) {
  501. throw new PatchApplyException(MessageFormat.format(
  502. JGitText.get().applyBinaryResultOidWrong,
  503. path));
  504. }
  505. }
  506. }
  507. try (InputStream bufIn = buffer.openInputStream()) {
  508. Files.copy(bufIn, f.toPath(),
  509. StandardCopyOption.REPLACE_EXISTING);
  510. }
  511. break;
  512. default:
  513. break;
  514. }
  515. } finally {
  516. buffer.destroy();
  517. }
  518. }
  519. private void applyText(Repository repository, String path, RawText rt,
  520. File f, FileHeader fh, CheckoutMetadata checkOut)
  521. throws IOException, PatchApplyException {
  522. List<ByteBuffer> oldLines = new ArrayList<>(rt.size());
  523. for (int i = 0; i < rt.size(); i++) {
  524. oldLines.add(rt.getRawString(i));
  525. }
  526. List<ByteBuffer> newLines = new ArrayList<>(oldLines);
  527. int afterLastHunk = 0;
  528. int lineNumberShift = 0;
  529. int lastHunkNewLine = -1;
  530. for (HunkHeader hh : fh.getHunks()) {
  531. // We assume hunks to be ordered
  532. if (hh.getNewStartLine() <= lastHunkNewLine) {
  533. throw new PatchApplyException(MessageFormat
  534. .format(JGitText.get().patchApplyException, hh));
  535. }
  536. lastHunkNewLine = hh.getNewStartLine();
  537. byte[] b = new byte[hh.getEndOffset() - hh.getStartOffset()];
  538. System.arraycopy(hh.getBuffer(), hh.getStartOffset(), b, 0,
  539. b.length);
  540. RawText hrt = new RawText(b);
  541. List<ByteBuffer> hunkLines = new ArrayList<>(hrt.size());
  542. for (int i = 0; i < hrt.size(); i++) {
  543. hunkLines.add(hrt.getRawString(i));
  544. }
  545. if (hh.getNewStartLine() == 0) {
  546. // Must be the single hunk for clearing all content
  547. if (fh.getHunks().size() == 1
  548. && canApplyAt(hunkLines, newLines, 0)) {
  549. newLines.clear();
  550. break;
  551. }
  552. throw new PatchApplyException(MessageFormat
  553. .format(JGitText.get().patchApplyException, hh));
  554. }
  555. // Hunk lines as reported by the hunk may be off, so don't rely on
  556. // them.
  557. int applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
  558. // But they definitely should not go backwards.
  559. if (applyAt < afterLastHunk && lineNumberShift < 0) {
  560. applyAt = hh.getNewStartLine() - 1;
  561. lineNumberShift = 0;
  562. }
  563. if (applyAt < afterLastHunk) {
  564. throw new PatchApplyException(MessageFormat
  565. .format(JGitText.get().patchApplyException, hh));
  566. }
  567. boolean applies = false;
  568. int oldLinesInHunk = hh.getLinesContext()
  569. + hh.getOldImage().getLinesDeleted();
  570. if (oldLinesInHunk <= 1) {
  571. // Don't shift hunks without context lines. Just try the
  572. // position corrected by the current lineNumberShift, and if
  573. // that fails, the position recorded in the hunk header.
  574. applies = canApplyAt(hunkLines, newLines, applyAt);
  575. if (!applies && lineNumberShift != 0) {
  576. applyAt = hh.getNewStartLine() - 1;
  577. applies = applyAt >= afterLastHunk
  578. && canApplyAt(hunkLines, newLines, applyAt);
  579. }
  580. } else {
  581. int maxShift = applyAt - afterLastHunk;
  582. for (int shift = 0; shift <= maxShift; shift++) {
  583. if (canApplyAt(hunkLines, newLines, applyAt - shift)) {
  584. applies = true;
  585. applyAt -= shift;
  586. break;
  587. }
  588. }
  589. if (!applies) {
  590. // Try shifting the hunk downwards
  591. applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
  592. maxShift = newLines.size() - applyAt - oldLinesInHunk;
  593. for (int shift = 1; shift <= maxShift; shift++) {
  594. if (canApplyAt(hunkLines, newLines, applyAt + shift)) {
  595. applies = true;
  596. applyAt += shift;
  597. break;
  598. }
  599. }
  600. }
  601. }
  602. if (!applies) {
  603. throw new PatchApplyException(MessageFormat
  604. .format(JGitText.get().patchApplyException, hh));
  605. }
  606. // Hunk applies at applyAt. Apply it, and update afterLastHunk and
  607. // lineNumberShift
  608. lineNumberShift = applyAt - hh.getNewStartLine() + 1;
  609. int sz = hunkLines.size();
  610. for (int j = 1; j < sz; j++) {
  611. ByteBuffer hunkLine = hunkLines.get(j);
  612. switch (hunkLine.array()[hunkLine.position()]) {
  613. case ' ':
  614. applyAt++;
  615. break;
  616. case '-':
  617. newLines.remove(applyAt);
  618. break;
  619. case '+':
  620. newLines.add(applyAt++, slice(hunkLine, 1));
  621. break;
  622. default:
  623. break;
  624. }
  625. }
  626. afterLastHunk = applyAt;
  627. }
  628. if (!isNoNewlineAtEndOfFile(fh)) {
  629. newLines.add(null);
  630. }
  631. if (!rt.isMissingNewlineAtEnd()) {
  632. oldLines.add(null);
  633. }
  634. if (oldLines.equals(newLines)) {
  635. return; // Unchanged; don't touch the file
  636. }
  637. TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
  638. try {
  639. try (OutputStream out = buffer) {
  640. for (Iterator<ByteBuffer> l = newLines.iterator(); l
  641. .hasNext();) {
  642. ByteBuffer line = l.next();
  643. if (line == null) {
  644. // Must be the marker for the final newline
  645. break;
  646. }
  647. out.write(line.array(), line.position(),
  648. line.limit() - line.position());
  649. if (l.hasNext()) {
  650. out.write('\n');
  651. }
  652. }
  653. }
  654. try (OutputStream output = new FileOutputStream(f)) {
  655. DirCacheCheckout.getContent(repository, path, checkOut,
  656. new StreamLoader(buffer::openInputStream,
  657. buffer.length()),
  658. null, output);
  659. }
  660. } finally {
  661. buffer.destroy();
  662. }
  663. repository.getFS().setExecute(f,
  664. fh.getNewMode() == FileMode.EXECUTABLE_FILE);
  665. }
  666. private boolean canApplyAt(List<ByteBuffer> hunkLines,
  667. List<ByteBuffer> newLines, int line) {
  668. int sz = hunkLines.size();
  669. int limit = newLines.size();
  670. int pos = line;
  671. for (int j = 1; j < sz; j++) {
  672. ByteBuffer hunkLine = hunkLines.get(j);
  673. switch (hunkLine.array()[hunkLine.position()]) {
  674. case ' ':
  675. case '-':
  676. if (pos >= limit
  677. || !newLines.get(pos).equals(slice(hunkLine, 1))) {
  678. return false;
  679. }
  680. pos++;
  681. break;
  682. default:
  683. break;
  684. }
  685. }
  686. return true;
  687. }
  688. private ByteBuffer slice(ByteBuffer b, int off) {
  689. int newOffset = b.position() + off;
  690. return ByteBuffer.wrap(b.array(), newOffset, b.limit() - newOffset);
  691. }
  692. private boolean isNoNewlineAtEndOfFile(FileHeader fh) {
  693. List<? extends HunkHeader> hunks = fh.getHunks();
  694. if (hunks == null || hunks.isEmpty()) {
  695. return false;
  696. }
  697. HunkHeader lastHunk = hunks.get(hunks.size() - 1);
  698. RawText lhrt = new RawText(lastHunk.getBuffer());
  699. return lhrt.getString(lhrt.size() - 1)
  700. .equals("\\ No newline at end of file"); //$NON-NLS-1$
  701. }
  702. /**
  703. * An {@link InputStream} that updates a {@link SHA1} on every byte read.
  704. * The hash is supposed to have been initialized before reading starts.
  705. */
  706. private static class SHA1InputStream extends InputStream {
  707. private final SHA1 hash;
  708. private final InputStream in;
  709. SHA1InputStream(SHA1 hash, InputStream in) {
  710. this.hash = hash;
  711. this.in = in;
  712. }
  713. @Override
  714. public int read() throws IOException {
  715. int b = in.read();
  716. if (b >= 0) {
  717. hash.update((byte) b);
  718. }
  719. return b;
  720. }
  721. @Override
  722. public int read(byte[] b, int off, int len) throws IOException {
  723. int n = in.read(b, off, len);
  724. if (n > 0) {
  725. hash.update(b, off, n);
  726. }
  727. return n;
  728. }
  729. @Override
  730. public void close() throws IOException {
  731. in.close();
  732. }
  733. }
  734. }