View Javadoc
1   /*
2    * Copyright (C) 2008-2013, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.diff;
45  
46  import java.io.IOException;
47  import java.util.ArrayList;
48  import java.util.Arrays;
49  import java.util.List;
50  
51  import org.eclipse.jgit.attributes.Attribute;
52  import org.eclipse.jgit.internal.JGitText;
53  import org.eclipse.jgit.lib.AbbreviatedObjectId;
54  import org.eclipse.jgit.lib.AnyObjectId;
55  import org.eclipse.jgit.lib.Constants;
56  import org.eclipse.jgit.lib.FileMode;
57  import org.eclipse.jgit.lib.MutableObjectId;
58  import org.eclipse.jgit.lib.ObjectId;
59  import org.eclipse.jgit.treewalk.TreeWalk;
60  import org.eclipse.jgit.treewalk.filter.TreeFilter;
61  import org.eclipse.jgit.treewalk.filter.TreeFilterMarker;
62  
63  /**
64   * A value class representing a change to a file
65   */
66  public class DiffEntry {
67  	/** Magical SHA1 used for file adds or deletes */
68  	static final AbbreviatedObjectId A_ZERO = AbbreviatedObjectId
69  			.fromObjectId(ObjectId.zeroId());
70  
71  	/** Magical file name used for file adds or deletes. */
72  	public static final String DEV_NULL = "/dev/null"; //$NON-NLS-1$
73  
74  	/** General type of change a single file-level patch describes. */
75  	public static enum ChangeType {
76  		/** Add a new file to the project */
77  		ADD,
78  
79  		/** Modify an existing file in the project (content and/or mode) */
80  		MODIFY,
81  
82  		/** Delete an existing file from the project */
83  		DELETE,
84  
85  		/** Rename an existing file to a new location */
86  		RENAME,
87  
88  		/** Copy an existing file to a new location, keeping the original */
89  		COPY;
90  	}
91  
92  	/** Specify the old or new side for more generalized access. */
93  	public static enum Side {
94  		/** The old side of a DiffEntry. */
95  		OLD,
96  
97  		/** The new side of a DiffEntry. */
98  		NEW;
99  	}
100 
101 	/**
102 	 * Create an empty DiffEntry
103 	 */
104 	protected DiffEntry(){
105 		// reduce the visibility of the default constructor
106 	}
107 
108 	/**
109 	 * Convert the TreeWalk into DiffEntry headers.
110 	 *
111 	 * @param walk
112 	 *            the TreeWalk to walk through. Must have exactly two trees.
113 	 * @return headers describing the changed files.
114 	 * @throws java.io.IOException
115 	 *             the repository cannot be accessed.
116 	 * @throws java.lang.IllegalArgumentException
117 	 *             When given TreeWalk doesn't have exactly two trees.
118 	 */
119 	public static List<DiffEntry> scan(TreeWalk walk) throws IOException {
120 		return scan(walk, false);
121 	}
122 
123 	/**
124 	 * Convert the TreeWalk into DiffEntry headers, depending on
125 	 * {@code includeTrees} it will add tree objects into result or not.
126 	 *
127 	 * @param walk
128 	 *            the TreeWalk to walk through. Must have exactly two trees and
129 	 *            when {@code includeTrees} parameter is {@code true} it can't
130 	 *            be recursive.
131 	 * @param includeTrees
132 	 *            include tree objects.
133 	 * @return headers describing the changed files.
134 	 * @throws java.io.IOException
135 	 *             the repository cannot be accessed.
136 	 * @throws java.lang.IllegalArgumentException
137 	 *             when {@code includeTrees} is true and given TreeWalk is
138 	 *             recursive. Or when given TreeWalk doesn't have exactly two
139 	 *             trees
140 	 */
141 	public static List<DiffEntry> scan(TreeWalk walk, boolean includeTrees)
142 			throws IOException {
143 		return scan(walk, includeTrees, null);
144 	}
145 
146 	/**
147 	 * Convert the TreeWalk into DiffEntry headers, depending on
148 	 * {@code includeTrees} it will add tree objects into result or not.
149 	 *
150 	 * @param walk
151 	 *            the TreeWalk to walk through. Must have exactly two trees and
152 	 *            when {@code includeTrees} parameter is {@code true} it can't
153 	 *            be recursive.
154 	 * @param includeTrees
155 	 *            include tree objects.
156 	 * @param markTreeFilters
157 	 *            array of tree filters which will be tested for each entry. If
158 	 *            an entry matches, the entry will later return true when
159 	 *            queried through {{@link #isMarked(int)} (with the index from
160 	 *            this passed array).
161 	 * @return headers describing the changed files.
162 	 * @throws java.io.IOException
163 	 *             the repository cannot be accessed.
164 	 * @throws java.lang.IllegalArgumentException
165 	 *             when {@code includeTrees} is true and given TreeWalk is
166 	 *             recursive. Or when given TreeWalk doesn't have exactly two
167 	 *             trees
168 	 * @since 2.3
169 	 */
170 	public static List<DiffEntry> scan(TreeWalk walk, boolean includeTrees,
171 			TreeFilter[] markTreeFilters)
172 			throws IOException {
173 		if (walk.getTreeCount() != 2)
174 			throw new IllegalArgumentException(
175 					JGitText.get().treeWalkMustHaveExactlyTwoTrees);
176 		if (includeTrees && walk.isRecursive())
177 			throw new IllegalArgumentException(
178 					JGitText.get().cannotBeRecursiveWhenTreesAreIncluded);
179 
180 		TreeFilterMarker treeFilterMarker;
181 		if (markTreeFilters != null && markTreeFilters.length > 0)
182 			treeFilterMarker = new TreeFilterMarker(markTreeFilters);
183 		else
184 			treeFilterMarker = null;
185 
186 		List<DiffEntry> r = new ArrayList<>();
187 		MutableObjectId idBuf = new MutableObjectId();
188 		while (walk.next()) {
189 			DiffEntry entry = new DiffEntry();
190 
191 			walk.getObjectId(idBuf, 0);
192 			entry.oldId = AbbreviatedObjectId.fromObjectId(idBuf);
193 
194 			walk.getObjectId(idBuf, 1);
195 			entry.newId = AbbreviatedObjectId.fromObjectId(idBuf);
196 
197 			entry.oldMode = walk.getFileMode(0);
198 			entry.newMode = walk.getFileMode(1);
199 			entry.newPath = entry.oldPath = walk.getPathString();
200 
201 			if (walk.getAttributesNodeProvider() != null) {
202 				entry.diffAttribute = walk.getAttributes()
203 						.get(Constants.ATTR_DIFF);
204 			}
205 
206 			if (treeFilterMarker != null)
207 				entry.treeFilterMarks = treeFilterMarker.getMarks(walk);
208 
209 			if (entry.oldMode == FileMode.MISSING) {
210 				entry.oldPath = DiffEntry.DEV_NULL;
211 				entry.changeType = ChangeType.ADD;
212 				r.add(entry);
213 
214 			} else if (entry.newMode == FileMode.MISSING) {
215 				entry.newPath = DiffEntry.DEV_NULL;
216 				entry.changeType = ChangeType.DELETE;
217 				r.add(entry);
218 
219 			} else if (!entry.oldId.equals(entry.newId)) {
220 				entry.changeType = ChangeType.MODIFY;
221 				if (RenameDetector.sameType(entry.oldMode, entry.newMode))
222 					r.add(entry);
223 				else
224 					r.addAll(breakModify(entry));
225 			} else if (entry.oldMode != entry.newMode) {
226 				entry.changeType = ChangeType.MODIFY;
227 				r.add(entry);
228 			}
229 
230 			if (includeTrees && walk.isSubtree())
231 				walk.enterSubtree();
232 		}
233 		return r;
234 	}
235 
236 	static DiffEntry add(String path, AnyObjectId id) {
237 		DiffEntry e = new DiffEntry();
238 		e.oldId = A_ZERO;
239 		e.oldMode = FileMode.MISSING;
240 		e.oldPath = DEV_NULL;
241 
242 		e.newId = AbbreviatedObjectId.fromObjectId(id);
243 		e.newMode = FileMode.REGULAR_FILE;
244 		e.newPath = path;
245 		e.changeType = ChangeType.ADD;
246 		return e;
247 	}
248 
249 	static DiffEntry delete(String path, AnyObjectId id) {
250 		DiffEntry e = new DiffEntry();
251 		e.oldId = AbbreviatedObjectId.fromObjectId(id);
252 		e.oldMode = FileMode.REGULAR_FILE;
253 		e.oldPath = path;
254 
255 		e.newId = A_ZERO;
256 		e.newMode = FileMode.MISSING;
257 		e.newPath = DEV_NULL;
258 		e.changeType = ChangeType.DELETE;
259 		return e;
260 	}
261 
262 	static DiffEntry modify(String path) {
263 		DiffEntry e = new DiffEntry();
264 		e.oldMode = FileMode.REGULAR_FILE;
265 		e.oldPath = path;
266 
267 		e.newMode = FileMode.REGULAR_FILE;
268 		e.newPath = path;
269 		e.changeType = ChangeType.MODIFY;
270 		return e;
271 	}
272 
273 	/**
274 	 * Breaks apart a DiffEntry into two entries, one DELETE and one ADD.
275 	 *
276 	 * @param entry
277 	 *            the DiffEntry to break apart.
278 	 * @return a list containing two entries. Calling {@link #getChangeType()}
279 	 *         on the first entry will return ChangeType.DELETE. Calling it on
280 	 *         the second entry will return ChangeType.ADD.
281 	 */
282 	static List<DiffEntry> breakModify(DiffEntry entry) {
283 		DiffEntry del = new DiffEntry();
284 		del.oldId = entry.getOldId();
285 		del.oldMode = entry.getOldMode();
286 		del.oldPath = entry.getOldPath();
287 
288 		del.newId = A_ZERO;
289 		del.newMode = FileMode.MISSING;
290 		del.newPath = DiffEntry.DEV_NULL;
291 		del.changeType = ChangeType.DELETE;
292 		del.diffAttribute = entry.diffAttribute;
293 
294 		DiffEntry add = new DiffEntry();
295 		add.oldId = A_ZERO;
296 		add.oldMode = FileMode.MISSING;
297 		add.oldPath = DiffEntry.DEV_NULL;
298 
299 		add.newId = entry.getNewId();
300 		add.newMode = entry.getNewMode();
301 		add.newPath = entry.getNewPath();
302 		add.changeType = ChangeType.ADD;
303 		add.diffAttribute = entry.diffAttribute;
304 		return Arrays.asList(del, add);
305 	}
306 
307 	static DiffEntry pair(ChangeType changeType, DiffEntry src, DiffEntry dst,
308 			int score) {
309 		DiffEntry r = new DiffEntry();
310 
311 		r.oldId = src.oldId;
312 		r.oldMode = src.oldMode;
313 		r.oldPath = src.oldPath;
314 
315 		r.newId = dst.newId;
316 		r.newMode = dst.newMode;
317 		r.newPath = dst.newPath;
318 		r.diffAttribute = dst.diffAttribute;
319 
320 		r.changeType = changeType;
321 		r.score = score;
322 
323 		r.treeFilterMarks = src.treeFilterMarks | dst.treeFilterMarks;
324 
325 		return r;
326 	}
327 
328 	/** File name of the old (pre-image). */
329 	protected String oldPath;
330 
331 	/** File name of the new (post-image). */
332 	protected String newPath;
333 
334 	/**
335 	 * diff filter attribute
336 	 *
337 	 * @since 4.11
338 	 */
339 	protected Attribute diffAttribute;
340 
341 	/** Old mode of the file, if described by the patch, else null. */
342 	protected FileMode oldMode;
343 
344 	/** New mode of the file, if described by the patch, else null. */
345 	protected FileMode newMode;
346 
347 	/** General type of change indicated by the patch. */
348 	protected ChangeType changeType;
349 
350 	/** Similarity score if {@link #changeType} is a copy or rename. */
351 	protected int score;
352 
353 	/** ObjectId listed on the index line for the old (pre-image) */
354 	protected AbbreviatedObjectId oldId;
355 
356 	/** ObjectId listed on the index line for the new (post-image) */
357 	protected AbbreviatedObjectId newId;
358 
359 	/**
360 	 * Bitset for marked flags of tree filters passed to
361 	 * {@link #scan(TreeWalk, boolean, TreeFilter...)}
362 	 */
363 	private int treeFilterMarks = 0;
364 
365 	/**
366 	 * Get the old name associated with this file.
367 	 * <p>
368 	 * The meaning of the old name can differ depending on the semantic meaning
369 	 * of this patch:
370 	 * <ul>
371 	 * <li><i>file add</i>: always <code>/dev/null</code></li>
372 	 * <li><i>file modify</i>: always {@link #getNewPath()}</li>
373 	 * <li><i>file delete</i>: always the file being deleted</li>
374 	 * <li><i>file copy</i>: source file the copy originates from</li>
375 	 * <li><i>file rename</i>: source file the rename originates from</li>
376 	 * </ul>
377 	 *
378 	 * @return old name for this file.
379 	 */
380 	public String getOldPath() {
381 		return oldPath;
382 	}
383 
384 	/**
385 	 * Get the new name associated with this file.
386 	 * <p>
387 	 * The meaning of the new name can differ depending on the semantic meaning
388 	 * of this patch:
389 	 * <ul>
390 	 * <li><i>file add</i>: always the file being created</li>
391 	 * <li><i>file modify</i>: always {@link #getOldPath()}</li>
392 	 * <li><i>file delete</i>: always <code>/dev/null</code></li>
393 	 * <li><i>file copy</i>: destination file the copy ends up at</li>
394 	 * <li><i>file rename</i>: destination file the rename ends up at</li>
395 	 * </ul>
396 	 *
397 	 * @return new name for this file.
398 	 */
399 	public String getNewPath() {
400 		return newPath;
401 	}
402 
403 	/**
404 	 * Get the path associated with this file.
405 	 *
406 	 * @param side
407 	 *            which path to obtain.
408 	 * @return name for this file.
409 	 */
410 	public String getPath(Side side) {
411 		return side == Side.OLD ? getOldPath() : getNewPath();
412 	}
413 
414 	/**
415 	 * @return the {@link Attribute} determining filters to be applied.
416 	 * @since 4.11
417 	 */
418 	public Attribute getDiffAttribute() {
419 		return diffAttribute;
420 	}
421 
422 	/**
423 	 * Get the old file mode
424 	 *
425 	 * @return the old file mode, if described in the patch
426 	 */
427 	public FileMode getOldMode() {
428 		return oldMode;
429 	}
430 
431 	/**
432 	 * Get the new file mode
433 	 *
434 	 * @return the new file mode, if described in the patch
435 	 */
436 	public FileMode getNewMode() {
437 		return newMode;
438 	}
439 
440 	/**
441 	 * Get the mode associated with this file.
442 	 *
443 	 * @param side
444 	 *            which mode to obtain.
445 	 * @return the mode.
446 	 */
447 	public FileMode getMode(Side side) {
448 		return side == Side.OLD ? getOldMode() : getNewMode();
449 	}
450 
451 	/**
452 	 * Get the change type
453 	 *
454 	 * @return the type of change this patch makes on {@link #getNewPath()}
455 	 */
456 	public ChangeType getChangeType() {
457 		return changeType;
458 	}
459 
460 	/**
461 	 * Get similarity score
462 	 *
463 	 * @return similarity score between {@link #getOldPath()} and
464 	 *         {@link #getNewPath()} if {@link #getChangeType()} is
465 	 *         {@link org.eclipse.jgit.diff.DiffEntry.ChangeType#COPY} or
466 	 *         {@link org.eclipse.jgit.diff.DiffEntry.ChangeType#RENAME}.
467 	 */
468 	public int getScore() {
469 		return score;
470 	}
471 
472 	/**
473 	 * Get the old object id from the <code>index</code>.
474 	 *
475 	 * @return the object id; null if there is no index line
476 	 */
477 	public AbbreviatedObjectId getOldId() {
478 		return oldId;
479 	}
480 
481 	/**
482 	 * Get the new object id from the <code>index</code>.
483 	 *
484 	 * @return the object id; null if there is no index line
485 	 */
486 	public AbbreviatedObjectId getNewId() {
487 		return newId;
488 	}
489 
490 	/**
491 	 * Whether the mark tree filter with the specified index matched during scan
492 	 * or not, see {@link #scan(TreeWalk, boolean, TreeFilter...)}. Example:
493 	 * <p>
494 	 *
495 	 * <pre>
496 	 * TreeFilter filterA = ...;
497 	 * TreeFilter filterB = ...;
498 	 * List&lt;DiffEntry&gt; entries = DiffEntry.scan(walk, false, filterA, filterB);
499 	 * DiffEntry entry = entries.get(0);
500 	 * boolean filterAMatched = entry.isMarked(0);
501 	 * boolean filterBMatched = entry.isMarked(1);
502 	 * </pre>
503 	 * <p>
504 	 * Note that 0 corresponds to filterA because it was the first filter that
505 	 * was passed to scan.
506 	 * <p>
507 	 * To query more than one flag at once, see {@link #getTreeFilterMarks()}.
508 	 *
509 	 * @param index
510 	 *            the index of the tree filter to check for (must be between 0
511 	 *            and {@link java.lang.Integer#SIZE}).
512 	 * @since 2.3
513 	 * @return a boolean.
514 	 */
515 	public boolean isMarked(int index) {
516 		return (treeFilterMarks & (1L << index)) != 0;
517 	}
518 
519 	/**
520 	 * Get the raw tree filter marks, as set during
521 	 * {@link #scan(TreeWalk, boolean, TreeFilter...)}. See
522 	 * {@link #isMarked(int)} to query each mark individually.
523 	 *
524 	 * @return the bitset of tree filter marks
525 	 * @since 2.3
526 	 */
527 	public int getTreeFilterMarks() {
528 		return treeFilterMarks;
529 	}
530 
531 	/**
532 	 * Get the object id.
533 	 *
534 	 * @param side
535 	 *            the side of the id to get.
536 	 * @return the object id; null if there is no index line
537 	 */
538 	public AbbreviatedObjectId getId(Side side) {
539 		return side == Side.OLD ? getOldId() : getNewId();
540 	}
541 
542 	/** {@inheritDoc} */
543 	@SuppressWarnings("nls")
544 	@Override
545 	public String toString() {
546 		StringBuilder buf = new StringBuilder();
547 		buf.append("DiffEntry[");
548 		buf.append(changeType);
549 		buf.append(" ");
550 		switch (changeType) {
551 		case ADD:
552 			buf.append(newPath);
553 			break;
554 		case COPY:
555 			buf.append(oldPath + "->" + newPath);
556 			break;
557 		case DELETE:
558 			buf.append(oldPath);
559 			break;
560 		case MODIFY:
561 			buf.append(oldPath);
562 			break;
563 		case RENAME:
564 			buf.append(oldPath + "->" + newPath);
565 			break;
566 		}
567 		buf.append("]");
568 		return buf.toString();
569 	}
570 }