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.

DiffFormatter.java 36KB

Show submodule difference as a hunk Current DiffFormat behavior regarding submodules (aka git links) is incorrect. The "Subproject commit <sha1>" appears as part of the diff header, rather than as its own hunk. --> From JGit implementation diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin index b9d3ca8..ec6ed89 160000 --- a/plugins/cookbook-plugin +++ b/plugins/cookbook-plugin -Subproject commit b9d3ca8a65030071e28be19296ba867ab424fbbf +Subproject commit ec6ed89c47ba7223f82d9cb512926a6c5081343e --> From C Git 2.5.2 diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin index b9d3ca8..ec6ed89 160000 --- a/plugins/cookbook-plugin +++ b/plugins/cookbook-plugin @@ -1 +1 @@ -Subproject commit b9d3ca8a65030071e28be19296ba867ab424fbbf +Subproject commit ec6ed89c47ba7223f82d9cb512926a6c5081343e The current way of processing submodules results in no hunk header and includes the contents of the hunk as part of the headers. To fix this, we can't just have our writeGitLinkDiffText output the hunk header. We have to change the flow so that the raw text gets parsed as a diff. The easiest way to do this is to fake the RawText in the FormatResult when we have a GITLINK. It should be noted that it seems possible for there to be a difference between a GITLINK and a non-GITLINK, but I don't think this can happen in practice, so I don't think we need to worry too much about it. This patch also fixes up the test for GitLink headers, as the test was for the old behavior. My setup has 3 other failing tests which may or may not be the result of environmental changes. However, the same tests fail without this commit, so I do not believe they are related. Bug: 477759 Change-Id: If13f7b406904fad814416c93ed09ea47ef183337 Signed-off-by: Jacob Keller <jacob.keller@gmail.com>
8 vuotta sitten
Show submodule difference as a hunk Current DiffFormat behavior regarding submodules (aka git links) is incorrect. The "Subproject commit <sha1>" appears as part of the diff header, rather than as its own hunk. --> From JGit implementation diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin index b9d3ca8..ec6ed89 160000 --- a/plugins/cookbook-plugin +++ b/plugins/cookbook-plugin -Subproject commit b9d3ca8a65030071e28be19296ba867ab424fbbf +Subproject commit ec6ed89c47ba7223f82d9cb512926a6c5081343e --> From C Git 2.5.2 diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin index b9d3ca8..ec6ed89 160000 --- a/plugins/cookbook-plugin +++ b/plugins/cookbook-plugin @@ -1 +1 @@ -Subproject commit b9d3ca8a65030071e28be19296ba867ab424fbbf +Subproject commit ec6ed89c47ba7223f82d9cb512926a6c5081343e The current way of processing submodules results in no hunk header and includes the contents of the hunk as part of the headers. To fix this, we can't just have our writeGitLinkDiffText output the hunk header. We have to change the flow so that the raw text gets parsed as a diff. The easiest way to do this is to fake the RawText in the FormatResult when we have a GITLINK. It should be noted that it seems possible for there to be a difference between a GITLINK and a non-GITLINK, but I don't think this can happen in practice, so I don't think we need to worry too much about it. This patch also fixes up the test for GitLink headers, as the test was for the old behavior. My setup has 3 other failing tests which may or may not be the result of environmental changes. However, the same tests fail without this commit, so I do not believe they are related. Bug: 477759 Change-Id: If13f7b406904fad814416c93ed09ea47ef183337 Signed-off-by: Jacob Keller <jacob.keller@gmail.com>
8 vuotta sitten
Show submodule difference as a hunk Current DiffFormat behavior regarding submodules (aka git links) is incorrect. The "Subproject commit <sha1>" appears as part of the diff header, rather than as its own hunk. --> From JGit implementation diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin index b9d3ca8..ec6ed89 160000 --- a/plugins/cookbook-plugin +++ b/plugins/cookbook-plugin -Subproject commit b9d3ca8a65030071e28be19296ba867ab424fbbf +Subproject commit ec6ed89c47ba7223f82d9cb512926a6c5081343e --> From C Git 2.5.2 diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin index b9d3ca8..ec6ed89 160000 --- a/plugins/cookbook-plugin +++ b/plugins/cookbook-plugin @@ -1 +1 @@ -Subproject commit b9d3ca8a65030071e28be19296ba867ab424fbbf +Subproject commit ec6ed89c47ba7223f82d9cb512926a6c5081343e The current way of processing submodules results in no hunk header and includes the contents of the hunk as part of the headers. To fix this, we can't just have our writeGitLinkDiffText output the hunk header. We have to change the flow so that the raw text gets parsed as a diff. The easiest way to do this is to fake the RawText in the FormatResult when we have a GITLINK. It should be noted that it seems possible for there to be a difference between a GITLINK and a non-GITLINK, but I don't think this can happen in practice, so I don't think we need to worry too much about it. This patch also fixes up the test for GitLink headers, as the test was for the old behavior. My setup has 3 other failing tests which may or may not be the result of environmental changes. However, the same tests fail without this commit, so I do not believe they are related. Bug: 477759 Change-Id: If13f7b406904fad814416c93ed09ea47ef183337 Signed-off-by: Jacob Keller <jacob.keller@gmail.com>
8 vuotta sitten
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205
  1. /*
  2. * Copyright (C) 2009, Google Inc.
  3. * Copyright (C) 2008-2009, Johannes E. Schindelin <johannes.schindelin@gmx.de>
  4. * and other copyright owners as documented in the project's IP log.
  5. *
  6. * This program and the accompanying materials are made available
  7. * under the terms of the Eclipse Distribution License v1.0 which
  8. * accompanies this distribution, is reproduced below, and is
  9. * available at http://www.eclipse.org/org/documents/edl-v10.php
  10. *
  11. * All rights reserved.
  12. *
  13. * Redistribution and use in source and binary forms, with or
  14. * without modification, are permitted provided that the following
  15. * conditions are met:
  16. *
  17. * - Redistributions of source code must retain the above copyright
  18. * notice, this list of conditions and the following disclaimer.
  19. *
  20. * - Redistributions in binary form must reproduce the above
  21. * copyright notice, this list of conditions and the following
  22. * disclaimer in the documentation and/or other materials provided
  23. * with the distribution.
  24. *
  25. * - Neither the name of the Eclipse Foundation, Inc. nor the
  26. * names of its contributors may be used to endorse or promote
  27. * products derived from this software without specific prior
  28. * written permission.
  29. *
  30. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  31. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  32. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  33. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  34. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  35. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  36. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  37. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  38. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  39. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  40. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  41. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  42. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  43. */
  44. package org.eclipse.jgit.diff;
  45. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
  46. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
  47. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
  48. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
  49. import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
  50. import static org.eclipse.jgit.diff.DiffEntry.Side.NEW;
  51. import static org.eclipse.jgit.diff.DiffEntry.Side.OLD;
  52. import static org.eclipse.jgit.lib.Constants.encode;
  53. import static org.eclipse.jgit.lib.Constants.encodeASCII;
  54. import static org.eclipse.jgit.lib.FileMode.GITLINK;
  55. import java.io.ByteArrayOutputStream;
  56. import java.io.IOException;
  57. import java.io.OutputStream;
  58. import java.util.Collection;
  59. import java.util.Collections;
  60. import java.util.List;
  61. import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
  62. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  63. import org.eclipse.jgit.dircache.DirCacheIterator;
  64. import org.eclipse.jgit.errors.AmbiguousObjectException;
  65. import org.eclipse.jgit.errors.CorruptObjectException;
  66. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  67. import org.eclipse.jgit.errors.LargeObjectException;
  68. import org.eclipse.jgit.errors.MissingObjectException;
  69. import org.eclipse.jgit.internal.JGitText;
  70. import org.eclipse.jgit.lib.AbbreviatedObjectId;
  71. import org.eclipse.jgit.lib.AnyObjectId;
  72. import org.eclipse.jgit.lib.ConfigConstants;
  73. import org.eclipse.jgit.lib.Constants;
  74. import org.eclipse.jgit.lib.FileMode;
  75. import org.eclipse.jgit.lib.ObjectId;
  76. import org.eclipse.jgit.lib.ObjectLoader;
  77. import org.eclipse.jgit.lib.ObjectReader;
  78. import org.eclipse.jgit.lib.ProgressMonitor;
  79. import org.eclipse.jgit.lib.Repository;
  80. import org.eclipse.jgit.patch.FileHeader;
  81. import org.eclipse.jgit.patch.FileHeader.PatchType;
  82. import org.eclipse.jgit.patch.HunkHeader;
  83. import org.eclipse.jgit.revwalk.FollowFilter;
  84. import org.eclipse.jgit.revwalk.RevTree;
  85. import org.eclipse.jgit.revwalk.RevWalk;
  86. import org.eclipse.jgit.storage.pack.PackConfig;
  87. import org.eclipse.jgit.treewalk.AbstractTreeIterator;
  88. import org.eclipse.jgit.treewalk.CanonicalTreeParser;
  89. import org.eclipse.jgit.treewalk.EmptyTreeIterator;
  90. import org.eclipse.jgit.treewalk.TreeWalk;
  91. import org.eclipse.jgit.treewalk.WorkingTreeIterator;
  92. import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
  93. import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
  94. import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
  95. import org.eclipse.jgit.treewalk.filter.PathFilter;
  96. import org.eclipse.jgit.treewalk.filter.TreeFilter;
  97. import org.eclipse.jgit.util.QuotedString;
  98. import org.eclipse.jgit.util.io.DisabledOutputStream;
  99. /**
  100. * Format a Git style patch script.
  101. */
  102. public class DiffFormatter implements AutoCloseable {
  103. private static final int DEFAULT_BINARY_FILE_THRESHOLD = PackConfig.DEFAULT_BIG_FILE_THRESHOLD;
  104. private static final byte[] noNewLine = encodeASCII("\\ No newline at end of file\n"); //$NON-NLS-1$
  105. /** Magic return content indicating it is empty or no content present. */
  106. private static final byte[] EMPTY = new byte[] {};
  107. /** Magic return indicating the content is binary. */
  108. private static final byte[] BINARY = new byte[] {};
  109. private final OutputStream out;
  110. private Repository db;
  111. private ObjectReader reader;
  112. private DiffConfig diffCfg;
  113. private int context = 3;
  114. private int abbreviationLength = 7;
  115. private DiffAlgorithm diffAlgorithm;
  116. private RawTextComparator comparator = RawTextComparator.DEFAULT;
  117. private int binaryFileThreshold = DEFAULT_BINARY_FILE_THRESHOLD;
  118. private String oldPrefix = "a/"; //$NON-NLS-1$
  119. private String newPrefix = "b/"; //$NON-NLS-1$
  120. private TreeFilter pathFilter = TreeFilter.ALL;
  121. private RenameDetector renameDetector;
  122. private ProgressMonitor progressMonitor;
  123. private ContentSource.Pair source;
  124. /**
  125. * Create a new formatter with a default level of context.
  126. *
  127. * @param out
  128. * the stream the formatter will write line data to. This stream
  129. * should have buffering arranged by the caller, as many small
  130. * writes are performed to it.
  131. */
  132. public DiffFormatter(OutputStream out) {
  133. this.out = out;
  134. }
  135. /** @return the stream we are outputting data to. */
  136. protected OutputStream getOutputStream() {
  137. return out;
  138. }
  139. /**
  140. * Set the repository the formatter can load object contents from.
  141. *
  142. * Once a repository has been set, the formatter must be released to ensure
  143. * the internal ObjectReader is able to release its resources.
  144. *
  145. * @param repository
  146. * source repository holding referenced objects.
  147. */
  148. public void setRepository(Repository repository) {
  149. if (reader != null)
  150. reader.close();
  151. db = repository;
  152. reader = db.newObjectReader();
  153. diffCfg = db.getConfig().get(DiffConfig.KEY);
  154. ContentSource cs = ContentSource.create(reader);
  155. source = new ContentSource.Pair(cs, cs);
  156. DiffConfig dc = db.getConfig().get(DiffConfig.KEY);
  157. if (dc.isNoPrefix()) {
  158. setOldPrefix(""); //$NON-NLS-1$
  159. setNewPrefix(""); //$NON-NLS-1$
  160. }
  161. setDetectRenames(dc.isRenameDetectionEnabled());
  162. diffAlgorithm = DiffAlgorithm.getAlgorithm(db.getConfig().getEnum(
  163. ConfigConstants.CONFIG_DIFF_SECTION, null,
  164. ConfigConstants.CONFIG_KEY_ALGORITHM,
  165. SupportedAlgorithm.HISTOGRAM));
  166. }
  167. /**
  168. * Change the number of lines of context to display.
  169. *
  170. * @param lineCount
  171. * number of lines of context to see before the first
  172. * modification and after the last modification within a hunk of
  173. * the modified file.
  174. */
  175. public void setContext(final int lineCount) {
  176. if (lineCount < 0)
  177. throw new IllegalArgumentException(
  178. JGitText.get().contextMustBeNonNegative);
  179. context = lineCount;
  180. }
  181. /**
  182. * Change the number of digits to show in an ObjectId.
  183. *
  184. * @param count
  185. * number of digits to show in an ObjectId.
  186. */
  187. public void setAbbreviationLength(final int count) {
  188. if (count < 0)
  189. throw new IllegalArgumentException(
  190. JGitText.get().abbreviationLengthMustBeNonNegative);
  191. abbreviationLength = count;
  192. }
  193. /**
  194. * Set the algorithm that constructs difference output.
  195. *
  196. * @param alg
  197. * the algorithm to produce text file differences.
  198. * @see HistogramDiff
  199. */
  200. public void setDiffAlgorithm(DiffAlgorithm alg) {
  201. diffAlgorithm = alg;
  202. }
  203. /**
  204. * Set the line equivalence function for text file differences.
  205. *
  206. * @param cmp
  207. * The equivalence function used to determine if two lines of
  208. * text are identical. The function can be changed to ignore
  209. * various types of whitespace.
  210. * @see RawTextComparator#DEFAULT
  211. * @see RawTextComparator#WS_IGNORE_ALL
  212. * @see RawTextComparator#WS_IGNORE_CHANGE
  213. * @see RawTextComparator#WS_IGNORE_LEADING
  214. * @see RawTextComparator#WS_IGNORE_TRAILING
  215. */
  216. public void setDiffComparator(RawTextComparator cmp) {
  217. comparator = cmp;
  218. }
  219. /**
  220. * Set maximum file size for text files.
  221. *
  222. * Files larger than this size will be treated as though they are binary and
  223. * not text. Default is {@value #DEFAULT_BINARY_FILE_THRESHOLD} .
  224. *
  225. * @param threshold
  226. * the limit, in bytes. Files larger than this size will be
  227. * assumed to be binary, even if they aren't.
  228. */
  229. public void setBinaryFileThreshold(int threshold) {
  230. this.binaryFileThreshold = threshold;
  231. }
  232. /**
  233. * Set the prefix applied in front of old file paths.
  234. *
  235. * @param prefix
  236. * the prefix in front of old paths. Typically this is the
  237. * standard string {@code "a/"}, but may be any prefix desired by
  238. * the caller. Must not be null. Use the empty string to have no
  239. * prefix at all.
  240. */
  241. public void setOldPrefix(String prefix) {
  242. oldPrefix = prefix;
  243. }
  244. /**
  245. * Get the prefix applied in front of old file paths.
  246. *
  247. * @return the prefix
  248. * @since 2.0
  249. */
  250. public String getOldPrefix() {
  251. return this.oldPrefix;
  252. }
  253. /**
  254. * Set the prefix applied in front of new file paths.
  255. *
  256. * @param prefix
  257. * the prefix in front of new paths. Typically this is the
  258. * standard string {@code "b/"}, but may be any prefix desired by
  259. * the caller. Must not be null. Use the empty string to have no
  260. * prefix at all.
  261. */
  262. public void setNewPrefix(String prefix) {
  263. newPrefix = prefix;
  264. }
  265. /**
  266. * Get the prefix applied in front of new file paths.
  267. *
  268. * @return the prefix
  269. * @since 2.0
  270. */
  271. public String getNewPrefix() {
  272. return this.newPrefix;
  273. }
  274. /** @return true if rename detection is enabled. */
  275. public boolean isDetectRenames() {
  276. return renameDetector != null;
  277. }
  278. /**
  279. * Enable or disable rename detection.
  280. *
  281. * Before enabling rename detection the repository must be set with
  282. * {@link #setRepository(Repository)}. Once enabled the detector can be
  283. * configured away from its defaults by obtaining the instance directly from
  284. * {@link #getRenameDetector()} and invoking configuration.
  285. *
  286. * @param on
  287. * if rename detection should be enabled.
  288. */
  289. public void setDetectRenames(boolean on) {
  290. if (on && renameDetector == null) {
  291. assertHaveRepository();
  292. renameDetector = new RenameDetector(db);
  293. } else if (!on)
  294. renameDetector = null;
  295. }
  296. /** @return the rename detector if rename detection is enabled. */
  297. public RenameDetector getRenameDetector() {
  298. return renameDetector;
  299. }
  300. /**
  301. * Set the progress monitor for long running rename detection.
  302. *
  303. * @param pm
  304. * progress monitor to receive rename detection status through.
  305. */
  306. public void setProgressMonitor(ProgressMonitor pm) {
  307. progressMonitor = pm;
  308. }
  309. /**
  310. * Set the filter to produce only specific paths.
  311. *
  312. * If the filter is an instance of {@link FollowFilter}, the filter path
  313. * will be updated during successive scan or format invocations. The updated
  314. * path can be obtained from {@link #getPathFilter()}.
  315. *
  316. * @param filter
  317. * the tree filter to apply.
  318. */
  319. public void setPathFilter(TreeFilter filter) {
  320. pathFilter = filter != null ? filter : TreeFilter.ALL;
  321. }
  322. /** @return the current path filter. */
  323. public TreeFilter getPathFilter() {
  324. return pathFilter;
  325. }
  326. /**
  327. * Flush the underlying output stream of this formatter.
  328. *
  329. * @throws IOException
  330. * the stream's own flush method threw an exception.
  331. */
  332. public void flush() throws IOException {
  333. out.flush();
  334. }
  335. /**
  336. * Release the internal ObjectReader state.
  337. *
  338. * @since 4.0
  339. */
  340. @Override
  341. public void close() {
  342. if (reader != null)
  343. reader.close();
  344. }
  345. /**
  346. * Determine the differences between two trees.
  347. *
  348. * No output is created, instead only the file paths that are different are
  349. * returned. Callers may choose to format these paths themselves, or convert
  350. * them into {@link FileHeader} instances with a complete edit list by
  351. * calling {@link #toFileHeader(DiffEntry)}.
  352. * <p>
  353. * Either side may be null to indicate that the tree has beed added or
  354. * removed. The diff will be computed against nothing.
  355. *
  356. * @param a
  357. * the old (or previous) side or null
  358. * @param b
  359. * the new (or updated) side or null
  360. * @return the paths that are different.
  361. * @throws IOException
  362. * trees cannot be read or file contents cannot be read.
  363. */
  364. public List<DiffEntry> scan(AnyObjectId a, AnyObjectId b)
  365. throws IOException {
  366. assertHaveRepository();
  367. try (RevWalk rw = new RevWalk(reader)) {
  368. RevTree aTree = a != null ? rw.parseTree(a) : null;
  369. RevTree bTree = b != null ? rw.parseTree(b) : null;
  370. return scan(aTree, bTree);
  371. }
  372. }
  373. /**
  374. * Determine the differences between two trees.
  375. *
  376. * No output is created, instead only the file paths that are different are
  377. * returned. Callers may choose to format these paths themselves, or convert
  378. * them into {@link FileHeader} instances with a complete edit list by
  379. * calling {@link #toFileHeader(DiffEntry)}.
  380. * <p>
  381. * Either side may be null to indicate that the tree has beed added or
  382. * removed. The diff will be computed against nothing.
  383. *
  384. * @param a
  385. * the old (or previous) side or null
  386. * @param b
  387. * the new (or updated) side or null
  388. * @return the paths that are different.
  389. * @throws IOException
  390. * trees cannot be read or file contents cannot be read.
  391. */
  392. public List<DiffEntry> scan(RevTree a, RevTree b) throws IOException {
  393. assertHaveRepository();
  394. AbstractTreeIterator aIterator = makeIteratorFromTreeOrNull(a);
  395. AbstractTreeIterator bIterator = makeIteratorFromTreeOrNull(b);
  396. return scan(aIterator, bIterator);
  397. }
  398. private AbstractTreeIterator makeIteratorFromTreeOrNull(RevTree tree)
  399. throws IncorrectObjectTypeException, IOException {
  400. if (tree != null) {
  401. CanonicalTreeParser parser = new CanonicalTreeParser();
  402. parser.reset(reader, tree);
  403. return parser;
  404. } else
  405. return new EmptyTreeIterator();
  406. }
  407. /**
  408. * Determine the differences between two trees.
  409. *
  410. * No output is created, instead only the file paths that are different are
  411. * returned. Callers may choose to format these paths themselves, or convert
  412. * them into {@link FileHeader} instances with a complete edit list by
  413. * calling {@link #toFileHeader(DiffEntry)}.
  414. *
  415. * @param a
  416. * the old (or previous) side.
  417. * @param b
  418. * the new (or updated) side.
  419. * @return the paths that are different.
  420. * @throws IOException
  421. * trees cannot be read or file contents cannot be read.
  422. */
  423. public List<DiffEntry> scan(AbstractTreeIterator a, AbstractTreeIterator b)
  424. throws IOException {
  425. assertHaveRepository();
  426. TreeWalk walk = new TreeWalk(reader);
  427. walk.addTree(a);
  428. walk.addTree(b);
  429. walk.setRecursive(true);
  430. TreeFilter filter = getDiffTreeFilterFor(a, b);
  431. if (pathFilter instanceof FollowFilter) {
  432. walk.setFilter(AndTreeFilter.create(
  433. PathFilter.create(((FollowFilter) pathFilter).getPath()),
  434. filter));
  435. } else {
  436. walk.setFilter(AndTreeFilter.create(pathFilter, filter));
  437. }
  438. source = new ContentSource.Pair(source(a), source(b));
  439. List<DiffEntry> files = DiffEntry.scan(walk);
  440. if (pathFilter instanceof FollowFilter && isAdd(files)) {
  441. // The file we are following was added here, find where it
  442. // came from so we can properly show the rename or copy,
  443. // then continue digging backwards.
  444. //
  445. a.reset();
  446. b.reset();
  447. walk.reset();
  448. walk.addTree(a);
  449. walk.addTree(b);
  450. walk.setFilter(filter);
  451. if (renameDetector == null)
  452. setDetectRenames(true);
  453. files = updateFollowFilter(detectRenames(DiffEntry.scan(walk)));
  454. } else if (renameDetector != null)
  455. files = detectRenames(files);
  456. return files;
  457. }
  458. private static TreeFilter getDiffTreeFilterFor(AbstractTreeIterator a,
  459. AbstractTreeIterator b) {
  460. if (a instanceof DirCacheIterator && b instanceof WorkingTreeIterator)
  461. return new IndexDiffFilter(0, 1);
  462. if (a instanceof WorkingTreeIterator && b instanceof DirCacheIterator)
  463. return new IndexDiffFilter(1, 0);
  464. TreeFilter filter = TreeFilter.ANY_DIFF;
  465. if (a instanceof WorkingTreeIterator)
  466. filter = AndTreeFilter.create(new NotIgnoredFilter(0), filter);
  467. if (b instanceof WorkingTreeIterator)
  468. filter = AndTreeFilter.create(new NotIgnoredFilter(1), filter);
  469. return filter;
  470. }
  471. private ContentSource source(AbstractTreeIterator iterator) {
  472. if (iterator instanceof WorkingTreeIterator)
  473. return ContentSource.create((WorkingTreeIterator) iterator);
  474. return ContentSource.create(reader);
  475. }
  476. private List<DiffEntry> detectRenames(List<DiffEntry> files)
  477. throws IOException {
  478. renameDetector.reset();
  479. renameDetector.addAll(files);
  480. return renameDetector.compute(reader, progressMonitor);
  481. }
  482. private boolean isAdd(List<DiffEntry> files) {
  483. String oldPath = ((FollowFilter) pathFilter).getPath();
  484. for (DiffEntry ent : files) {
  485. if (ent.getChangeType() == ADD && ent.getNewPath().equals(oldPath))
  486. return true;
  487. }
  488. return false;
  489. }
  490. private List<DiffEntry> updateFollowFilter(List<DiffEntry> files) {
  491. String oldPath = ((FollowFilter) pathFilter).getPath();
  492. for (DiffEntry ent : files) {
  493. if (isRename(ent) && ent.getNewPath().equals(oldPath)) {
  494. pathFilter = FollowFilter.create(ent.getOldPath(), diffCfg);
  495. return Collections.singletonList(ent);
  496. }
  497. }
  498. return Collections.emptyList();
  499. }
  500. private static boolean isRename(DiffEntry ent) {
  501. return ent.getChangeType() == RENAME || ent.getChangeType() == COPY;
  502. }
  503. /**
  504. * Format the differences between two trees.
  505. *
  506. * The patch is expressed as instructions to modify {@code a} to make it
  507. * {@code b}.
  508. * <p>
  509. * Either side may be null to indicate that the tree has beed added or
  510. * removed. The diff will be computed against nothing.
  511. *
  512. * @param a
  513. * the old (or previous) side or null
  514. * @param b
  515. * the new (or updated) side or null
  516. * @throws IOException
  517. * trees cannot be read, file contents cannot be read, or the
  518. * patch cannot be output.
  519. */
  520. public void format(AnyObjectId a, AnyObjectId b) throws IOException {
  521. format(scan(a, b));
  522. }
  523. /**
  524. * Format the differences between two trees.
  525. *
  526. * The patch is expressed as instructions to modify {@code a} to make it
  527. * {@code b}.
  528. *
  529. * <p>
  530. * Either side may be null to indicate that the tree has beed added or
  531. * removed. The diff will be computed against nothing.
  532. *
  533. * @param a
  534. * the old (or previous) side or null
  535. * @param b
  536. * the new (or updated) side or null
  537. * @throws IOException
  538. * trees cannot be read, file contents cannot be read, or the
  539. * patch cannot be output.
  540. */
  541. public void format(RevTree a, RevTree b) throws IOException {
  542. format(scan(a, b));
  543. }
  544. /**
  545. * Format the differences between two trees.
  546. *
  547. * The patch is expressed as instructions to modify {@code a} to make it
  548. * {@code b}.
  549. * <p>
  550. * Either side may be null to indicate that the tree has beed added or
  551. * removed. The diff will be computed against nothing.
  552. *
  553. * @param a
  554. * the old (or previous) side or null
  555. * @param b
  556. * the new (or updated) side or null
  557. * @throws IOException
  558. * trees cannot be read, file contents cannot be read, or the
  559. * patch cannot be output.
  560. */
  561. public void format(AbstractTreeIterator a, AbstractTreeIterator b)
  562. throws IOException {
  563. format(scan(a, b));
  564. }
  565. /**
  566. * Format a patch script from a list of difference entries. Requires
  567. * {@link #scan(AbstractTreeIterator, AbstractTreeIterator)} to have been
  568. * called first.
  569. *
  570. * @param entries
  571. * entries describing the affected files.
  572. * @throws IOException
  573. * a file's content cannot be read, or the output stream cannot
  574. * be written to.
  575. */
  576. public void format(List<? extends DiffEntry> entries) throws IOException {
  577. for (DiffEntry ent : entries)
  578. format(ent);
  579. }
  580. /**
  581. * Format a patch script for one file entry.
  582. *
  583. * @param ent
  584. * the entry to be formatted.
  585. * @throws IOException
  586. * a file's content cannot be read, or the output stream cannot
  587. * be written to.
  588. */
  589. public void format(DiffEntry ent) throws IOException {
  590. FormatResult res = createFormatResult(ent);
  591. format(res.header, res.a, res.b);
  592. }
  593. private static byte[] writeGitLinkText(AbbreviatedObjectId id) {
  594. if (id.toObjectId().equals(ObjectId.zeroId())) {
  595. return EMPTY;
  596. }
  597. return encodeASCII("Subproject commit " + id.name() //$NON-NLS-1$
  598. + "\n"); //$NON-NLS-1$
  599. }
  600. private String format(AbbreviatedObjectId id) {
  601. if (id.isComplete() && db != null) {
  602. try {
  603. id = reader.abbreviate(id.toObjectId(), abbreviationLength);
  604. } catch (IOException cannotAbbreviate) {
  605. // Ignore this. We'll report the full identity.
  606. }
  607. }
  608. return id.name();
  609. }
  610. private static String quotePath(String name) {
  611. return QuotedString.GIT_PATH.quote(name);
  612. }
  613. /**
  614. * Format a patch script, reusing a previously parsed FileHeader.
  615. * <p>
  616. * This formatter is primarily useful for editing an existing patch script
  617. * to increase or reduce the number of lines of context within the script.
  618. * All header lines are reused as-is from the supplied FileHeader.
  619. *
  620. * @param head
  621. * existing file header containing the header lines to copy.
  622. * @param a
  623. * text source for the pre-image version of the content. This
  624. * must match the content of {@link FileHeader#getOldId()}.
  625. * @param b
  626. * text source for the post-image version of the content. This
  627. * must match the content of {@link FileHeader#getNewId()}.
  628. * @throws IOException
  629. * writing to the supplied stream failed.
  630. */
  631. public void format(final FileHeader head, final RawText a, final RawText b)
  632. throws IOException {
  633. // Reuse the existing FileHeader as-is by blindly copying its
  634. // header lines, but avoiding its hunks. Instead we recreate
  635. // the hunks from the text instances we have been supplied.
  636. //
  637. final int start = head.getStartOffset();
  638. int end = head.getEndOffset();
  639. if (!head.getHunks().isEmpty())
  640. end = head.getHunks().get(0).getStartOffset();
  641. out.write(head.getBuffer(), start, end - start);
  642. if (head.getPatchType() == PatchType.UNIFIED)
  643. format(head.toEditList(), a, b);
  644. }
  645. /**
  646. * Formats a list of edits in unified diff format
  647. *
  648. * @param edits
  649. * some differences which have been calculated between A and B
  650. * @param a
  651. * the text A which was compared
  652. * @param b
  653. * the text B which was compared
  654. * @throws IOException
  655. */
  656. public void format(final EditList edits, final RawText a, final RawText b)
  657. throws IOException {
  658. for (int curIdx = 0; curIdx < edits.size();) {
  659. Edit curEdit = edits.get(curIdx);
  660. final int endIdx = findCombinedEnd(edits, curIdx);
  661. final Edit endEdit = edits.get(endIdx);
  662. int aCur = (int) Math.max(0, (long) curEdit.getBeginA() - context);
  663. int bCur = (int) Math.max(0, (long) curEdit.getBeginB() - context);
  664. final int aEnd = (int) Math.min(a.size(), (long) endEdit.getEndA() + context);
  665. final int bEnd = (int) Math.min(b.size(), (long) endEdit.getEndB() + context);
  666. writeHunkHeader(aCur, aEnd, bCur, bEnd);
  667. while (aCur < aEnd || bCur < bEnd) {
  668. if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) {
  669. writeContextLine(a, aCur);
  670. if (isEndOfLineMissing(a, aCur))
  671. out.write(noNewLine);
  672. aCur++;
  673. bCur++;
  674. } else if (aCur < curEdit.getEndA()) {
  675. writeRemovedLine(a, aCur);
  676. if (isEndOfLineMissing(a, aCur))
  677. out.write(noNewLine);
  678. aCur++;
  679. } else if (bCur < curEdit.getEndB()) {
  680. writeAddedLine(b, bCur);
  681. if (isEndOfLineMissing(b, bCur))
  682. out.write(noNewLine);
  683. bCur++;
  684. }
  685. if (end(curEdit, aCur, bCur) && ++curIdx < edits.size())
  686. curEdit = edits.get(curIdx);
  687. }
  688. }
  689. }
  690. /**
  691. * Output a line of context (unmodified line).
  692. *
  693. * @param text
  694. * RawText for accessing raw data
  695. * @param line
  696. * the line number within text
  697. * @throws IOException
  698. */
  699. protected void writeContextLine(final RawText text, final int line)
  700. throws IOException {
  701. writeLine(' ', text, line);
  702. }
  703. private static boolean isEndOfLineMissing(final RawText text, final int line) {
  704. return line + 1 == text.size() && text.isMissingNewlineAtEnd();
  705. }
  706. /**
  707. * Output an added line.
  708. *
  709. * @param text
  710. * RawText for accessing raw data
  711. * @param line
  712. * the line number within text
  713. * @throws IOException
  714. */
  715. protected void writeAddedLine(final RawText text, final int line)
  716. throws IOException {
  717. writeLine('+', text, line);
  718. }
  719. /**
  720. * Output a removed line
  721. *
  722. * @param text
  723. * RawText for accessing raw data
  724. * @param line
  725. * the line number within text
  726. * @throws IOException
  727. */
  728. protected void writeRemovedLine(final RawText text, final int line)
  729. throws IOException {
  730. writeLine('-', text, line);
  731. }
  732. /**
  733. * Output a hunk header
  734. *
  735. * @param aStartLine
  736. * within first source
  737. * @param aEndLine
  738. * within first source
  739. * @param bStartLine
  740. * within second source
  741. * @param bEndLine
  742. * within second source
  743. * @throws IOException
  744. */
  745. protected void writeHunkHeader(int aStartLine, int aEndLine,
  746. int bStartLine, int bEndLine) throws IOException {
  747. out.write('@');
  748. out.write('@');
  749. writeRange('-', aStartLine + 1, aEndLine - aStartLine);
  750. writeRange('+', bStartLine + 1, bEndLine - bStartLine);
  751. out.write(' ');
  752. out.write('@');
  753. out.write('@');
  754. out.write('\n');
  755. }
  756. private void writeRange(final char prefix, final int begin, final int cnt)
  757. throws IOException {
  758. out.write(' ');
  759. out.write(prefix);
  760. switch (cnt) {
  761. case 0:
  762. // If the range is empty, its beginning number must be the
  763. // line just before the range, or 0 if the range is at the
  764. // start of the file stream. Here, begin is always 1 based,
  765. // so an empty file would produce "0,0".
  766. //
  767. out.write(encodeASCII(begin - 1));
  768. out.write(',');
  769. out.write('0');
  770. break;
  771. case 1:
  772. // If the range is exactly one line, produce only the number.
  773. //
  774. out.write(encodeASCII(begin));
  775. break;
  776. default:
  777. out.write(encodeASCII(begin));
  778. out.write(',');
  779. out.write(encodeASCII(cnt));
  780. break;
  781. }
  782. }
  783. /**
  784. * Write a standard patch script line.
  785. *
  786. * @param prefix
  787. * prefix before the line, typically '-', '+', ' '.
  788. * @param text
  789. * the text object to obtain the line from.
  790. * @param cur
  791. * line number to output.
  792. * @throws IOException
  793. * the stream threw an exception while writing to it.
  794. */
  795. protected void writeLine(final char prefix, final RawText text,
  796. final int cur) throws IOException {
  797. out.write(prefix);
  798. text.writeLine(out, cur);
  799. out.write('\n');
  800. }
  801. /**
  802. * Creates a {@link FileHeader} representing the given {@link DiffEntry}
  803. * <p>
  804. * This method does not use the OutputStream associated with this
  805. * DiffFormatter instance. It is therefore safe to instantiate this
  806. * DiffFormatter instance with a {@link DisabledOutputStream} if this method
  807. * is the only one that will be used.
  808. *
  809. * @param ent
  810. * the DiffEntry to create the FileHeader for
  811. * @return a FileHeader representing the DiffEntry. The FileHeader's buffer
  812. * will contain only the header of the diff output. It will also
  813. * contain one {@link HunkHeader}.
  814. * @throws IOException
  815. * the stream threw an exception while writing to it, or one of
  816. * the blobs referenced by the DiffEntry could not be read.
  817. * @throws CorruptObjectException
  818. * one of the blobs referenced by the DiffEntry is corrupt.
  819. * @throws MissingObjectException
  820. * one of the blobs referenced by the DiffEntry is missing.
  821. */
  822. public FileHeader toFileHeader(DiffEntry ent) throws IOException,
  823. CorruptObjectException, MissingObjectException {
  824. return createFormatResult(ent).header;
  825. }
  826. private static class FormatResult {
  827. FileHeader header;
  828. RawText a;
  829. RawText b;
  830. }
  831. private FormatResult createFormatResult(DiffEntry ent) throws IOException,
  832. CorruptObjectException, MissingObjectException {
  833. final FormatResult res = new FormatResult();
  834. ByteArrayOutputStream buf = new ByteArrayOutputStream();
  835. final EditList editList;
  836. final FileHeader.PatchType type;
  837. formatHeader(buf, ent);
  838. if (ent.getOldId() == null || ent.getNewId() == null) {
  839. // Content not changed (e.g. only mode, pure rename)
  840. editList = new EditList();
  841. type = PatchType.UNIFIED;
  842. } else {
  843. assertHaveRepository();
  844. byte[] aRaw, bRaw;
  845. if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
  846. aRaw = writeGitLinkText(ent.getOldId());
  847. bRaw = writeGitLinkText(ent.getNewId());
  848. } else {
  849. aRaw = open(OLD, ent);
  850. bRaw = open(NEW, ent);
  851. }
  852. if (aRaw == BINARY || bRaw == BINARY //
  853. || RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
  854. formatOldNewPaths(buf, ent);
  855. buf.write(encodeASCII("Binary files differ\n")); //$NON-NLS-1$
  856. editList = new EditList();
  857. type = PatchType.BINARY;
  858. } else {
  859. res.a = new RawText(aRaw);
  860. res.b = new RawText(bRaw);
  861. editList = diff(res.a, res.b);
  862. type = PatchType.UNIFIED;
  863. switch (ent.getChangeType()) {
  864. case RENAME:
  865. case COPY:
  866. if (!editList.isEmpty())
  867. formatOldNewPaths(buf, ent);
  868. break;
  869. default:
  870. formatOldNewPaths(buf, ent);
  871. break;
  872. }
  873. }
  874. }
  875. res.header = new FileHeader(buf.toByteArray(), editList, type);
  876. return res;
  877. }
  878. private EditList diff(RawText a, RawText b) {
  879. return diffAlgorithm.diff(comparator, a, b);
  880. }
  881. private void assertHaveRepository() {
  882. if (db == null)
  883. throw new IllegalStateException(JGitText.get().repositoryIsRequired);
  884. }
  885. private byte[] open(DiffEntry.Side side, DiffEntry entry)
  886. throws IOException {
  887. if (entry.getMode(side) == FileMode.MISSING)
  888. return EMPTY;
  889. if (entry.getMode(side).getObjectType() != Constants.OBJ_BLOB)
  890. return EMPTY;
  891. AbbreviatedObjectId id = entry.getId(side);
  892. if (!id.isComplete()) {
  893. Collection<ObjectId> ids = reader.resolve(id);
  894. if (ids.size() == 1) {
  895. id = AbbreviatedObjectId.fromObjectId(ids.iterator().next());
  896. switch (side) {
  897. case OLD:
  898. entry.oldId = id;
  899. break;
  900. case NEW:
  901. entry.newId = id;
  902. break;
  903. }
  904. } else if (ids.size() == 0)
  905. throw new MissingObjectException(id, Constants.OBJ_BLOB);
  906. else
  907. throw new AmbiguousObjectException(id, ids);
  908. }
  909. try {
  910. ObjectLoader ldr = source.open(side, entry);
  911. return ldr.getBytes(binaryFileThreshold);
  912. } catch (LargeObjectException.ExceedsLimit overLimit) {
  913. return BINARY;
  914. } catch (LargeObjectException.ExceedsByteArrayLimit overLimit) {
  915. return BINARY;
  916. } catch (LargeObjectException.OutOfMemory tooBig) {
  917. return BINARY;
  918. } catch (LargeObjectException tooBig) {
  919. tooBig.setObjectId(id.toObjectId());
  920. throw tooBig;
  921. }
  922. }
  923. /**
  924. * Output the first header line
  925. *
  926. * @param o
  927. * The stream the formatter will write the first header line to
  928. * @param type
  929. * The {@link ChangeType}
  930. * @param oldPath
  931. * old path to the file
  932. * @param newPath
  933. * new path to the file
  934. * @throws IOException
  935. * the stream threw an exception while writing to it.
  936. */
  937. protected void formatGitDiffFirstHeaderLine(ByteArrayOutputStream o,
  938. final ChangeType type, final String oldPath, final String newPath)
  939. throws IOException {
  940. o.write(encodeASCII("diff --git ")); //$NON-NLS-1$
  941. o.write(encode(quotePath(oldPrefix + (type == ADD ? newPath : oldPath))));
  942. o.write(' ');
  943. o.write(encode(quotePath(newPrefix
  944. + (type == DELETE ? oldPath : newPath))));
  945. o.write('\n');
  946. }
  947. private void formatHeader(ByteArrayOutputStream o, DiffEntry ent)
  948. throws IOException {
  949. final ChangeType type = ent.getChangeType();
  950. final String oldp = ent.getOldPath();
  951. final String newp = ent.getNewPath();
  952. final FileMode oldMode = ent.getOldMode();
  953. final FileMode newMode = ent.getNewMode();
  954. formatGitDiffFirstHeaderLine(o, type, oldp, newp);
  955. if ((type == MODIFY || type == COPY || type == RENAME)
  956. && !oldMode.equals(newMode)) {
  957. o.write(encodeASCII("old mode ")); //$NON-NLS-1$
  958. oldMode.copyTo(o);
  959. o.write('\n');
  960. o.write(encodeASCII("new mode ")); //$NON-NLS-1$
  961. newMode.copyTo(o);
  962. o.write('\n');
  963. }
  964. switch (type) {
  965. case ADD:
  966. o.write(encodeASCII("new file mode ")); //$NON-NLS-1$
  967. newMode.copyTo(o);
  968. o.write('\n');
  969. break;
  970. case DELETE:
  971. o.write(encodeASCII("deleted file mode ")); //$NON-NLS-1$
  972. oldMode.copyTo(o);
  973. o.write('\n');
  974. break;
  975. case RENAME:
  976. o.write(encodeASCII("similarity index " + ent.getScore() + "%")); //$NON-NLS-1$ //$NON-NLS-2$
  977. o.write('\n');
  978. o.write(encode("rename from " + quotePath(oldp))); //$NON-NLS-1$
  979. o.write('\n');
  980. o.write(encode("rename to " + quotePath(newp))); //$NON-NLS-1$
  981. o.write('\n');
  982. break;
  983. case COPY:
  984. o.write(encodeASCII("similarity index " + ent.getScore() + "%")); //$NON-NLS-1$ //$NON-NLS-2$
  985. o.write('\n');
  986. o.write(encode("copy from " + quotePath(oldp))); //$NON-NLS-1$
  987. o.write('\n');
  988. o.write(encode("copy to " + quotePath(newp))); //$NON-NLS-1$
  989. o.write('\n');
  990. break;
  991. case MODIFY:
  992. if (0 < ent.getScore()) {
  993. o.write(encodeASCII("dissimilarity index " //$NON-NLS-1$
  994. + (100 - ent.getScore()) + "%")); //$NON-NLS-1$
  995. o.write('\n');
  996. }
  997. break;
  998. }
  999. if (ent.getOldId() != null && !ent.getOldId().equals(ent.getNewId())) {
  1000. formatIndexLine(o, ent);
  1001. }
  1002. }
  1003. /**
  1004. * @param o
  1005. * the stream the formatter will write line data to
  1006. * @param ent
  1007. * the DiffEntry to create the FileHeader for
  1008. * @throws IOException
  1009. * writing to the supplied stream failed.
  1010. */
  1011. protected void formatIndexLine(OutputStream o, DiffEntry ent)
  1012. throws IOException {
  1013. o.write(encodeASCII("index " // //$NON-NLS-1$
  1014. + format(ent.getOldId()) //
  1015. + ".." // //$NON-NLS-1$
  1016. + format(ent.getNewId())));
  1017. if (ent.getOldMode().equals(ent.getNewMode())) {
  1018. o.write(' ');
  1019. ent.getNewMode().copyTo(o);
  1020. }
  1021. o.write('\n');
  1022. }
  1023. private void formatOldNewPaths(ByteArrayOutputStream o, DiffEntry ent)
  1024. throws IOException {
  1025. if (ent.oldId.equals(ent.newId))
  1026. return;
  1027. final String oldp;
  1028. final String newp;
  1029. switch (ent.getChangeType()) {
  1030. case ADD:
  1031. oldp = DiffEntry.DEV_NULL;
  1032. newp = quotePath(newPrefix + ent.getNewPath());
  1033. break;
  1034. case DELETE:
  1035. oldp = quotePath(oldPrefix + ent.getOldPath());
  1036. newp = DiffEntry.DEV_NULL;
  1037. break;
  1038. default:
  1039. oldp = quotePath(oldPrefix + ent.getOldPath());
  1040. newp = quotePath(newPrefix + ent.getNewPath());
  1041. break;
  1042. }
  1043. o.write(encode("--- " + oldp + "\n")); //$NON-NLS-1$ //$NON-NLS-2$
  1044. o.write(encode("+++ " + newp + "\n")); //$NON-NLS-1$ //$NON-NLS-2$
  1045. }
  1046. private int findCombinedEnd(final List<Edit> edits, final int i) {
  1047. int end = i + 1;
  1048. while (end < edits.size()
  1049. && (combineA(edits, end) || combineB(edits, end)))
  1050. end++;
  1051. return end - 1;
  1052. }
  1053. private boolean combineA(final List<Edit> e, final int i) {
  1054. return e.get(i).getBeginA() - e.get(i - 1).getEndA() <= 2 * context;
  1055. }
  1056. private boolean combineB(final List<Edit> e, final int i) {
  1057. return e.get(i).getBeginB() - e.get(i - 1).getEndB() <= 2 * context;
  1058. }
  1059. private static boolean end(final Edit edit, final int a, final int b) {
  1060. return edit.getEndA() <= a && edit.getEndB() <= b;
  1061. }
  1062. }