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 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  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.BufferedWriter;
  12. import java.io.File;
  13. import java.io.FileInputStream;
  14. import java.io.FileOutputStream;
  15. import java.io.IOException;
  16. import java.io.InputStream;
  17. import java.io.OutputStream;
  18. import java.io.OutputStreamWriter;
  19. import java.io.Writer;
  20. import java.nio.charset.StandardCharsets;
  21. import java.nio.file.Files;
  22. import java.nio.file.StandardCopyOption;
  23. import java.text.MessageFormat;
  24. import java.util.ArrayList;
  25. import java.util.Iterator;
  26. import java.util.List;
  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.ObjectLoader;
  46. import org.eclipse.jgit.lib.ObjectStream;
  47. import org.eclipse.jgit.lib.Repository;
  48. import org.eclipse.jgit.patch.FileHeader;
  49. import org.eclipse.jgit.patch.HunkHeader;
  50. import org.eclipse.jgit.patch.Patch;
  51. import org.eclipse.jgit.treewalk.FileTreeIterator;
  52. import org.eclipse.jgit.treewalk.TreeWalk;
  53. import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
  54. import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
  55. import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
  56. import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
  57. import org.eclipse.jgit.util.FS;
  58. import org.eclipse.jgit.util.FileUtils;
  59. import org.eclipse.jgit.util.IO;
  60. import org.eclipse.jgit.util.RawParseUtils;
  61. import org.eclipse.jgit.util.StringUtils;
  62. import org.eclipse.jgit.util.TemporaryBuffer;
  63. import org.eclipse.jgit.util.FS.ExecutionResult;
  64. import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
  65. import org.eclipse.jgit.util.io.EolStreamTypeUtil;
  66. /**
  67. * Apply a patch to files and/or to the index.
  68. *
  69. * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-apply.html"
  70. * >Git documentation about apply</a>
  71. * @since 2.0
  72. */
  73. public class ApplyCommand extends GitCommand<ApplyResult> {
  74. private InputStream in;
  75. /**
  76. * Constructs the command.
  77. *
  78. * @param repo
  79. */
  80. ApplyCommand(Repository repo) {
  81. super(repo);
  82. }
  83. /**
  84. * Set patch
  85. *
  86. * @param in
  87. * the patch to apply
  88. * @return this instance
  89. */
  90. public ApplyCommand setPatch(InputStream in) {
  91. checkCallable();
  92. this.in = in;
  93. return this;
  94. }
  95. /**
  96. * {@inheritDoc}
  97. * <p>
  98. * Executes the {@code ApplyCommand} command with all the options and
  99. * parameters collected by the setter methods (e.g.
  100. * {@link #setPatch(InputStream)} of this class. Each instance of this class
  101. * should only be used for one invocation of the command. Don't call this
  102. * method twice on an instance.
  103. */
  104. @Override
  105. public ApplyResult call() throws GitAPIException, PatchFormatException,
  106. PatchApplyException {
  107. checkCallable();
  108. setCallable(false);
  109. ApplyResult r = new ApplyResult();
  110. try {
  111. final Patch p = new Patch();
  112. try {
  113. p.parse(in);
  114. } finally {
  115. in.close();
  116. }
  117. if (!p.getErrors().isEmpty()) {
  118. throw new PatchFormatException(p.getErrors());
  119. }
  120. Repository repository = getRepository();
  121. DirCache cache = repository.readDirCache();
  122. for (FileHeader fh : p.getFiles()) {
  123. ChangeType type = fh.getChangeType();
  124. File f = null;
  125. switch (type) {
  126. case ADD:
  127. f = getFile(fh.getNewPath(), true);
  128. apply(repository, fh.getNewPath(), cache, f, fh);
  129. break;
  130. case MODIFY:
  131. f = getFile(fh.getOldPath(), false);
  132. apply(repository, fh.getOldPath(), cache, f, fh);
  133. break;
  134. case DELETE:
  135. f = getFile(fh.getOldPath(), false);
  136. if (!f.delete())
  137. throw new PatchApplyException(MessageFormat.format(
  138. JGitText.get().cannotDeleteFile, f));
  139. break;
  140. case RENAME:
  141. f = getFile(fh.getOldPath(), false);
  142. File dest = getFile(fh.getNewPath(), false);
  143. try {
  144. FileUtils.mkdirs(dest.getParentFile(), true);
  145. FileUtils.rename(f, dest,
  146. StandardCopyOption.ATOMIC_MOVE);
  147. } catch (IOException e) {
  148. throw new PatchApplyException(MessageFormat.format(
  149. JGitText.get().renameFileFailed, f, dest), e);
  150. }
  151. apply(repository, fh.getOldPath(), cache, dest, fh);
  152. break;
  153. case COPY:
  154. f = getFile(fh.getOldPath(), false);
  155. File target = getFile(fh.getNewPath(), false);
  156. FileUtils.mkdirs(target.getParentFile(), true);
  157. Files.copy(f.toPath(), target.toPath());
  158. apply(repository, fh.getOldPath(), cache, target, fh);
  159. }
  160. r.addUpdatedFile(f);
  161. }
  162. } catch (IOException e) {
  163. throw new PatchApplyException(MessageFormat.format(
  164. JGitText.get().patchApplyException, e.getMessage()), e);
  165. }
  166. return r;
  167. }
  168. private File getFile(String path, boolean create)
  169. throws PatchApplyException {
  170. File f = new File(getRepository().getWorkTree(), path);
  171. if (create) {
  172. try {
  173. File parent = f.getParentFile();
  174. FileUtils.mkdirs(parent, true);
  175. FileUtils.createNewFile(f);
  176. } catch (IOException e) {
  177. throw new PatchApplyException(MessageFormat.format(
  178. JGitText.get().createNewFileFailed, f), e);
  179. }
  180. }
  181. return f;
  182. }
  183. private void apply(Repository repository, String path, DirCache cache,
  184. File f, FileHeader fh) throws IOException, PatchApplyException {
  185. boolean convertCrLf = needsCrLfConversion(f, fh);
  186. // Use a TreeWalk with a DirCacheIterator to pick up the correct
  187. // clean/smudge filters. CR-LF handling is completely determined by
  188. // whether the file or the patch have CR-LF line endings.
  189. try (TreeWalk walk = new TreeWalk(repository)) {
  190. walk.setOperationType(OperationType.CHECKIN_OP);
  191. FileTreeIterator files = new FileTreeIterator(repository);
  192. int fileIdx = walk.addTree(files);
  193. int cacheIdx = walk.addTree(new DirCacheIterator(cache));
  194. files.setDirCacheIterator(walk, cacheIdx);
  195. walk.setFilter(AndTreeFilter.create(
  196. PathFilterGroup.createFromStrings(path),
  197. new NotIgnoredFilter(fileIdx)));
  198. walk.setRecursive(true);
  199. if (walk.next()) {
  200. // If the file on disk has no newline characters, convertCrLf
  201. // will be false. In that case we want to honor the normal git
  202. // settings.
  203. EolStreamType streamType = convertCrLf ? EolStreamType.TEXT_CRLF
  204. : walk.getEolStreamType(OperationType.CHECKOUT_OP);
  205. String command = walk.getFilterCommand(
  206. Constants.ATTR_FILTER_TYPE_SMUDGE);
  207. CheckoutMetadata checkOut = new CheckoutMetadata(streamType, command);
  208. FileTreeIterator file = walk.getTree(fileIdx,
  209. FileTreeIterator.class);
  210. if (file != null) {
  211. command = walk
  212. .getFilterCommand(Constants.ATTR_FILTER_TYPE_CLEAN);
  213. RawText raw;
  214. // Can't use file.openEntryStream() as it would do CR-LF
  215. // conversion as usual, not as wanted by us.
  216. try (InputStream input = filterClean(repository, path,
  217. new FileInputStream(f), convertCrLf, command)) {
  218. raw = new RawText(IO.readWholeStream(input, 0).array());
  219. }
  220. apply(repository, path, raw, f, fh, checkOut);
  221. return;
  222. }
  223. }
  224. }
  225. // File ignored?
  226. RawText raw;
  227. CheckoutMetadata checkOut;
  228. if (convertCrLf) {
  229. try (InputStream input = EolStreamTypeUtil.wrapInputStream(
  230. new FileInputStream(f), EolStreamType.TEXT_LF)) {
  231. raw = new RawText(IO.readWholeStream(input, 0).array());
  232. }
  233. checkOut = new CheckoutMetadata(EolStreamType.TEXT_CRLF, null);
  234. } else {
  235. raw = new RawText(f);
  236. checkOut = new CheckoutMetadata(EolStreamType.DIRECT, null);
  237. }
  238. apply(repository, path, raw, f, fh, checkOut);
  239. }
  240. private boolean needsCrLfConversion(File f, FileHeader fileHeader)
  241. throws IOException {
  242. if (!hasCrLf(fileHeader)) {
  243. try (InputStream input = new FileInputStream(f)) {
  244. return RawText.isCrLfText(input);
  245. }
  246. }
  247. return false;
  248. }
  249. private static boolean hasCrLf(FileHeader fileHeader) {
  250. if (fileHeader == null) {
  251. return false;
  252. }
  253. for (HunkHeader header : fileHeader.getHunks()) {
  254. byte[] buf = header.getBuffer();
  255. int hunkEnd = header.getEndOffset();
  256. int lineStart = header.getStartOffset();
  257. while (lineStart < hunkEnd) {
  258. int nextLineStart = RawParseUtils.nextLF(buf, lineStart);
  259. if (nextLineStart > hunkEnd) {
  260. nextLineStart = hunkEnd;
  261. }
  262. if (nextLineStart <= lineStart) {
  263. break;
  264. }
  265. if (nextLineStart - lineStart > 1) {
  266. char first = (char) (buf[lineStart] & 0xFF);
  267. if (first == ' ' || first == '-') {
  268. // It's an old line. Does it end in CR-LF?
  269. if (buf[nextLineStart - 2] == '\r') {
  270. return true;
  271. }
  272. }
  273. }
  274. lineStart = nextLineStart;
  275. }
  276. }
  277. return false;
  278. }
  279. private InputStream filterClean(Repository repository, String path,
  280. InputStream fromFile, boolean convertCrLf, String filterCommand)
  281. throws IOException {
  282. InputStream input = fromFile;
  283. if (convertCrLf) {
  284. input = EolStreamTypeUtil.wrapInputStream(input,
  285. EolStreamType.TEXT_LF);
  286. }
  287. if (StringUtils.isEmptyOrNull(filterCommand)) {
  288. return input;
  289. }
  290. if (FilterCommandRegistry.isRegistered(filterCommand)) {
  291. LocalFile buffer = new TemporaryBuffer.LocalFile(null);
  292. FilterCommand command = FilterCommandRegistry.createFilterCommand(
  293. filterCommand, repository, input, buffer);
  294. while (command.run() != -1) {
  295. // loop as long as command.run() tells there is work to do
  296. }
  297. return buffer.openInputStreamWithAutoDestroy();
  298. }
  299. FS fs = repository.getFS();
  300. ProcessBuilder filterProcessBuilder = fs.runInShell(filterCommand,
  301. new String[0]);
  302. filterProcessBuilder.directory(repository.getWorkTree());
  303. filterProcessBuilder.environment().put(Constants.GIT_DIR_KEY,
  304. repository.getDirectory().getAbsolutePath());
  305. ExecutionResult result;
  306. try {
  307. result = fs.execute(filterProcessBuilder, in);
  308. } catch (IOException | InterruptedException e) {
  309. throw new IOException(
  310. new FilterFailedException(e, filterCommand, path));
  311. }
  312. int rc = result.getRc();
  313. if (rc != 0) {
  314. throw new IOException(new FilterFailedException(rc, filterCommand,
  315. path, result.getStdout().toByteArray(4096), RawParseUtils
  316. .decode(result.getStderr().toByteArray(4096))));
  317. }
  318. return result.getStdout().openInputStreamWithAutoDestroy();
  319. }
  320. /**
  321. * We write the patch result to a {@link TemporaryBuffer} and then use
  322. * {@link DirCacheCheckout}.getContent() to run the result through the CR-LF
  323. * and smudge filters. DirCacheCheckout needs an ObjectLoader, not a
  324. * TemporaryBuffer, so this class bridges between the two, making the
  325. * TemporaryBuffer look like an ordinary git blob to DirCacheCheckout.
  326. */
  327. private static class BufferLoader extends ObjectLoader {
  328. private TemporaryBuffer data;
  329. BufferLoader(TemporaryBuffer data) {
  330. this.data = data;
  331. }
  332. @Override
  333. public int getType() {
  334. return Constants.OBJ_BLOB;
  335. }
  336. @Override
  337. public long getSize() {
  338. return data.length();
  339. }
  340. @Override
  341. public boolean isLarge() {
  342. return true;
  343. }
  344. @Override
  345. public byte[] getCachedBytes() throws LargeObjectException {
  346. throw new LargeObjectException();
  347. }
  348. @Override
  349. public ObjectStream openStream()
  350. throws MissingObjectException, IOException {
  351. return new ObjectStream.Filter(getType(), getSize(),
  352. data.openInputStream());
  353. }
  354. }
  355. private void apply(Repository repository, String path, RawText rt, File f,
  356. FileHeader fh, CheckoutMetadata checkOut)
  357. throws IOException, PatchApplyException {
  358. List<String> oldLines = new ArrayList<>(rt.size());
  359. for (int i = 0; i < rt.size(); i++) {
  360. oldLines.add(rt.getString(i));
  361. }
  362. List<String> newLines = new ArrayList<>(oldLines);
  363. int afterLastHunk = 0;
  364. int lineNumberShift = 0;
  365. int lastHunkNewLine = -1;
  366. for (HunkHeader hh : fh.getHunks()) {
  367. // We assume hunks to be ordered
  368. if (hh.getNewStartLine() <= lastHunkNewLine) {
  369. throw new PatchApplyException(MessageFormat
  370. .format(JGitText.get().patchApplyException, hh));
  371. }
  372. lastHunkNewLine = hh.getNewStartLine();
  373. byte[] b = new byte[hh.getEndOffset() - hh.getStartOffset()];
  374. System.arraycopy(hh.getBuffer(), hh.getStartOffset(), b, 0,
  375. b.length);
  376. RawText hrt = new RawText(b);
  377. List<String> hunkLines = new ArrayList<>(hrt.size());
  378. for (int i = 0; i < hrt.size(); i++) {
  379. hunkLines.add(hrt.getString(i));
  380. }
  381. if (hh.getNewStartLine() == 0) {
  382. // Must be the single hunk for clearing all content
  383. if (fh.getHunks().size() == 1
  384. && canApplyAt(hunkLines, newLines, 0)) {
  385. newLines.clear();
  386. break;
  387. }
  388. throw new PatchApplyException(MessageFormat
  389. .format(JGitText.get().patchApplyException, hh));
  390. }
  391. // Hunk lines as reported by the hunk may be off, so don't rely on
  392. // them.
  393. int applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
  394. // But they definitely should not go backwards.
  395. if (applyAt < afterLastHunk && lineNumberShift < 0) {
  396. applyAt = hh.getNewStartLine() - 1;
  397. lineNumberShift = 0;
  398. }
  399. if (applyAt < afterLastHunk) {
  400. throw new PatchApplyException(MessageFormat
  401. .format(JGitText.get().patchApplyException, hh));
  402. }
  403. boolean applies = false;
  404. int oldLinesInHunk = hh.getLinesContext()
  405. + hh.getOldImage().getLinesDeleted();
  406. if (oldLinesInHunk <= 1) {
  407. // Don't shift hunks without context lines. Just try the
  408. // position corrected by the current lineNumberShift, and if
  409. // that fails, the position recorded in the hunk header.
  410. applies = canApplyAt(hunkLines, newLines, applyAt);
  411. if (!applies && lineNumberShift != 0) {
  412. applyAt = hh.getNewStartLine() - 1;
  413. applies = applyAt >= afterLastHunk
  414. && canApplyAt(hunkLines, newLines, applyAt);
  415. }
  416. } else {
  417. int maxShift = applyAt - afterLastHunk;
  418. for (int shift = 0; shift <= maxShift; shift++) {
  419. if (canApplyAt(hunkLines, newLines, applyAt - shift)) {
  420. applies = true;
  421. applyAt -= shift;
  422. break;
  423. }
  424. }
  425. if (!applies) {
  426. // Try shifting the hunk downwards
  427. applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
  428. maxShift = newLines.size() - applyAt - oldLinesInHunk;
  429. for (int shift = 1; shift <= maxShift; shift++) {
  430. if (canApplyAt(hunkLines, newLines, applyAt + shift)) {
  431. applies = true;
  432. applyAt += shift;
  433. break;
  434. }
  435. }
  436. }
  437. }
  438. if (!applies) {
  439. throw new PatchApplyException(MessageFormat
  440. .format(JGitText.get().patchApplyException, hh));
  441. }
  442. // Hunk applies at applyAt. Apply it, and update afterLastHunk and
  443. // lineNumberShift
  444. lineNumberShift = applyAt - hh.getNewStartLine() + 1;
  445. int sz = hunkLines.size();
  446. for (int j = 1; j < sz; j++) {
  447. String hunkLine = hunkLines.get(j);
  448. switch (hunkLine.charAt(0)) {
  449. case ' ':
  450. applyAt++;
  451. break;
  452. case '-':
  453. newLines.remove(applyAt);
  454. break;
  455. case '+':
  456. newLines.add(applyAt++, hunkLine.substring(1));
  457. break;
  458. default:
  459. break;
  460. }
  461. }
  462. afterLastHunk = applyAt;
  463. }
  464. if (!isNoNewlineAtEndOfFile(fh)) {
  465. newLines.add(""); //$NON-NLS-1$
  466. }
  467. if (!rt.isMissingNewlineAtEnd()) {
  468. oldLines.add(""); //$NON-NLS-1$
  469. }
  470. if (!isChanged(oldLines, newLines)) {
  471. return; // Don't touch the file
  472. }
  473. // TODO: forcing UTF-8 is a bit strange and may lead to re-coding if the
  474. // input was some other encoding, but it's what previous versions of
  475. // this code used. (Even earlier the code used the default encoding,
  476. // which has the same problem.) Perhaps using bytes instead of Strings
  477. // for the lines would be better.
  478. TemporaryBuffer buffer = new TemporaryBuffer.LocalFile(null);
  479. try {
  480. try (Writer w = new BufferedWriter(
  481. new OutputStreamWriter(buffer, StandardCharsets.UTF_8))) {
  482. for (Iterator<String> l = newLines.iterator(); l.hasNext();) {
  483. w.write(l.next());
  484. if (l.hasNext()) {
  485. w.write('\n');
  486. }
  487. }
  488. }
  489. try (OutputStream output = new FileOutputStream(f)) {
  490. DirCacheCheckout.getContent(repository, path, checkOut,
  491. new BufferLoader(buffer), null, output);
  492. }
  493. } finally {
  494. buffer.destroy();
  495. }
  496. repository.getFS().setExecute(f,
  497. fh.getNewMode() == FileMode.EXECUTABLE_FILE);
  498. }
  499. private boolean canApplyAt(List<String> hunkLines, List<String> newLines,
  500. int line) {
  501. int sz = hunkLines.size();
  502. int limit = newLines.size();
  503. int pos = line;
  504. for (int j = 1; j < sz; j++) {
  505. String hunkLine = hunkLines.get(j);
  506. switch (hunkLine.charAt(0)) {
  507. case ' ':
  508. case '-':
  509. if (pos >= limit
  510. || !newLines.get(pos).equals(hunkLine.substring(1))) {
  511. return false;
  512. }
  513. pos++;
  514. break;
  515. default:
  516. break;
  517. }
  518. }
  519. return true;
  520. }
  521. private static boolean isChanged(List<String> ol, List<String> nl) {
  522. if (ol.size() != nl.size())
  523. return true;
  524. for (int i = 0; i < ol.size(); i++)
  525. if (!ol.get(i).equals(nl.get(i)))
  526. return true;
  527. return false;
  528. }
  529. private boolean isNoNewlineAtEndOfFile(FileHeader fh) {
  530. List<? extends HunkHeader> hunks = fh.getHunks();
  531. if (hunks == null || hunks.isEmpty()) {
  532. return false;
  533. }
  534. HunkHeader lastHunk = hunks.get(hunks.size() - 1);
  535. RawText lhrt = new RawText(lastHunk.getBuffer());
  536. return lhrt.getString(lhrt.size() - 1)
  537. .equals("\\ No newline at end of file"); //$NON-NLS-1$
  538. }
  539. }