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.internal.JGitText;
52  import org.eclipse.jgit.lib.AbbreviatedObjectId;
53  import org.eclipse.jgit.lib.AnyObjectId;
54  import org.eclipse.jgit.lib.FileMode;
55  import org.eclipse.jgit.lib.MutableObjectId;
56  import org.eclipse.jgit.lib.ObjectId;
57  import org.eclipse.jgit.treewalk.TreeWalk;
58  import org.eclipse.jgit.treewalk.filter.TreeFilter;
59  import org.eclipse.jgit.treewalk.filter.TreeFilterMarker;
60  
61  /** A value class representing a change to a file */
62  public class DiffEntry {
63  	/** Magical SHA1 used for file adds or deletes */
64  	static final AbbreviatedObjectId A_ZERO = AbbreviatedObjectId
65  			.fromObjectId(ObjectId.zeroId());
66  
67  	/** Magical file name used for file adds or deletes. */
68  	public static final String DEV_NULL = "/dev/null"; //$NON-NLS-1$
69  
70  	/** General type of change a single file-level patch describes. */
71  	public static enum ChangeType {
72  		/** Add a new file to the project */
73  		ADD,
74  
75  		/** Modify an existing file in the project (content and/or mode) */
76  		MODIFY,
77  
78  		/** Delete an existing file from the project */
79  		DELETE,
80  
81  		/** Rename an existing file to a new location */
82  		RENAME,
83  
84  		/** Copy an existing file to a new location, keeping the original */
85  		COPY;
86  	}
87  
88  	/** Specify the old or new side for more generalized access. */
89  	public static enum Side {
90  		/** The old side of a DiffEntry. */
91  		OLD,
92  
93  		/** The new side of a DiffEntry. */
94  		NEW;
95  	}
96  
97  	/**
98  	 * Create an empty DiffEntry
99  	 */
100 	protected DiffEntry(){
101 		// reduce the visibility of the default constructor
102 	}
103 
104 	/**
105 	 * Convert the TreeWalk into DiffEntry headers.
106 	 *
107 	 * @param walk
108 	 *            the TreeWalk to walk through. Must have exactly two trees.
109 	 * @return headers describing the changed files.
110 	 * @throws IOException
111 	 *             the repository cannot be accessed.
112 	 * @throws IllegalArgumentException
113 	 *             When given TreeWalk doesn't have exactly two trees.
114 	 */
115 	public static List<DiffEntry> scan(TreeWalk walk) throws IOException {
116 		return scan(walk, false);
117 	}
118 
119 	/**
120 	 * Convert the TreeWalk into DiffEntry headers, depending on
121 	 * {@code includeTrees} it will add tree objects into result or not.
122 	 *
123 	 * @param walk
124 	 *            the TreeWalk to walk through. Must have exactly two trees and
125 	 *            when {@code includeTrees} parameter is {@code true} it can't
126 	 *            be recursive.
127 	 * @param includeTrees
128 	 *            include tree objects.
129 	 * @return headers describing the changed files.
130 	 * @throws IOException
131 	 *             the repository cannot be accessed.
132 	 * @throws IllegalArgumentException
133 	 *             when {@code includeTrees} is true and given TreeWalk is
134 	 *             recursive. Or when given TreeWalk doesn't have exactly two
135 	 *             trees
136 	 */
137 	public static List<DiffEntry> scan(TreeWalk walk, boolean includeTrees)
138 			throws IOException {
139 		return scan(walk, includeTrees, null);
140 	}
141 
142 	/**
143 	 * Convert the TreeWalk into DiffEntry headers, depending on
144 	 * {@code includeTrees} it will add tree objects into result or not.
145 	 *
146 	 * @param walk
147 	 *            the TreeWalk to walk through. Must have exactly two trees and
148 	 *            when {@code includeTrees} parameter is {@code true} it can't
149 	 *            be recursive.
150 	 * @param includeTrees
151 	 *            include tree objects.
152 	 * @param markTreeFilters
153 	 *            array of tree filters which will be tested for each entry. If
154 	 *            an entry matches, the entry will later return true when
155 	 *            queried through {{@link #isMarked(int)} (with the index from
156 	 *            this passed array).
157 	 * @return headers describing the changed files.
158 	 * @throws IOException
159 	 *             the repository cannot be accessed.
160 	 * @throws IllegalArgumentException
161 	 *             when {@code includeTrees} is true and given TreeWalk is
162 	 *             recursive. Or when given TreeWalk doesn't have exactly two
163 	 *             trees
164 	 * @since 2.3
165 	 */
166 	public static List<DiffEntry> scan(TreeWalk walk, boolean includeTrees,
167 			TreeFilter[] markTreeFilters)
168 			throws IOException {
169 		if (walk.getTreeCount() != 2)
170 			throw new IllegalArgumentException(
171 					JGitText.get().treeWalkMustHaveExactlyTwoTrees);
172 		if (includeTrees && walk.isRecursive())
173 			throw new IllegalArgumentException(
174 					JGitText.get().cannotBeRecursiveWhenTreesAreIncluded);
175 
176 		TreeFilterMarker treeFilterMarker;
177 		if (markTreeFilters != null && markTreeFilters.length > 0)
178 			treeFilterMarker = new TreeFilterMarker(markTreeFilters);
179 		else
180 			treeFilterMarker = null;
181 
182 		List<DiffEntry> r = new ArrayList<DiffEntry>();
183 		MutableObjectId idBuf = new MutableObjectId();
184 		while (walk.next()) {
185 			DiffEntry entry = new DiffEntry();
186 
187 			walk.getObjectId(idBuf, 0);
188 			entry.oldId = AbbreviatedObjectId.fromObjectId(idBuf);
189 
190 			walk.getObjectId(idBuf, 1);
191 			entry.newId = AbbreviatedObjectId.fromObjectId(idBuf);
192 
193 			entry.oldMode = walk.getFileMode(0);
194 			entry.newMode = walk.getFileMode(1);
195 			entry.newPath = entry.oldPath = walk.getPathString();
196 
197 			if (treeFilterMarker != null)
198 				entry.treeFilterMarks = treeFilterMarker.getMarks(walk);
199 
200 			if (entry.oldMode == FileMode.MISSING) {
201 				entry.oldPath = DiffEntry.DEV_NULL;
202 				entry.changeType = ChangeType.ADD;
203 				r.add(entry);
204 
205 			} else if (entry.newMode == FileMode.MISSING) {
206 				entry.newPath = DiffEntry.DEV_NULL;
207 				entry.changeType = ChangeType.DELETE;
208 				r.add(entry);
209 
210 			} else if (!entry.oldId.equals(entry.newId)) {
211 				entry.changeType = ChangeType.MODIFY;
212 				if (RenameDetector.sameType(entry.oldMode, entry.newMode))
213 					r.add(entry);
214 				else
215 					r.addAll(breakModify(entry));
216 			} else if (entry.oldMode != entry.newMode) {
217 				entry.changeType = ChangeType.MODIFY;
218 				r.add(entry);
219 			}
220 
221 			if (includeTrees && walk.isSubtree())
222 				walk.enterSubtree();
223 		}
224 		return r;
225 	}
226 
227 	static DiffEntry add(String path, AnyObjectId id) {
228 		DiffEntry e = new DiffEntry();
229 		e.oldId = A_ZERO;
230 		e.oldMode = FileMode.MISSING;
231 		e.oldPath = DEV_NULL;
232 
233 		e.newId = AbbreviatedObjectId.fromObjectId(id);
234 		e.newMode = FileMode.REGULAR_FILE;
235 		e.newPath = path;
236 		e.changeType = ChangeType.ADD;
237 		return e;
238 	}
239 
240 	static DiffEntry delete(String path, AnyObjectId id) {
241 		DiffEntry e = new DiffEntry();
242 		e.oldId = AbbreviatedObjectId.fromObjectId(id);
243 		e.oldMode = FileMode.REGULAR_FILE;
244 		e.oldPath = path;
245 
246 		e.newId = A_ZERO;
247 		e.newMode = FileMode.MISSING;
248 		e.newPath = DEV_NULL;
249 		e.changeType = ChangeType.DELETE;
250 		return e;
251 	}
252 
253 	static DiffEntry modify(String path) {
254 		DiffEntry e = new DiffEntry();
255 		e.oldMode = FileMode.REGULAR_FILE;
256 		e.oldPath = path;
257 
258 		e.newMode = FileMode.REGULAR_FILE;
259 		e.newPath = path;
260 		e.changeType = ChangeType.MODIFY;
261 		return e;
262 	}
263 
264 	/**
265 	 * Breaks apart a DiffEntry into two entries, one DELETE and one ADD.
266 	 *
267 	 * @param entry
268 	 *            the DiffEntry to break apart.
269 	 * @return a list containing two entries. Calling {@link #getChangeType()}
270 	 *         on the first entry will return ChangeType.DELETE. Calling it on
271 	 *         the second entry will return ChangeType.ADD.
272 	 */
273 	static List<DiffEntry> breakModify(DiffEntry entry) {
274 		DiffEntry del = new DiffEntry();
275 		del.oldId = entry.getOldId();
276 		del.oldMode = entry.getOldMode();
277 		del.oldPath = entry.getOldPath();
278 
279 		del.newId = A_ZERO;
280 		del.newMode = FileMode.MISSING;
281 		del.newPath = DiffEntry.DEV_NULL;
282 		del.changeType = ChangeType.DELETE;
283 
284 		DiffEntry add = new DiffEntry();
285 		add.oldId = A_ZERO;
286 		add.oldMode = FileMode.MISSING;
287 		add.oldPath = DiffEntry.DEV_NULL;
288 
289 		add.newId = entry.getNewId();
290 		add.newMode = entry.getNewMode();
291 		add.newPath = entry.getNewPath();
292 		add.changeType = ChangeType.ADD;
293 		return Arrays.asList(del, add);
294 	}
295 
296 	static DiffEntry pair(ChangeType changeType, DiffEntry src, DiffEntry dst,
297 			int score) {
298 		DiffEntry r = new DiffEntry();
299 
300 		r.oldId = src.oldId;
301 		r.oldMode = src.oldMode;
302 		r.oldPath = src.oldPath;
303 
304 		r.newId = dst.newId;
305 		r.newMode = dst.newMode;
306 		r.newPath = dst.newPath;
307 
308 		r.changeType = changeType;
309 		r.score = score;
310 
311 		r.treeFilterMarks = src.treeFilterMarks | dst.treeFilterMarks;
312 
313 		return r;
314 	}
315 
316 	/** File name of the old (pre-image). */
317 	protected String oldPath;
318 
319 	/** File name of the new (post-image). */
320 	protected String newPath;
321 
322 	/** Old mode of the file, if described by the patch, else null. */
323 	protected FileMode oldMode;
324 
325 	/** New mode of the file, if described by the patch, else null. */
326 	protected FileMode newMode;
327 
328 	/** General type of change indicated by the patch. */
329 	protected ChangeType changeType;
330 
331 	/** Similarity score if {@link #changeType} is a copy or rename. */
332 	protected int score;
333 
334 	/** ObjectId listed on the index line for the old (pre-image) */
335 	protected AbbreviatedObjectId oldId;
336 
337 	/** ObjectId listed on the index line for the new (post-image) */
338 	protected AbbreviatedObjectId newId;
339 
340 	/**
341 	 * Bitset for marked flags of tree filters passed to
342 	 * {@link #scan(TreeWalk, boolean, TreeFilter...)}
343 	 */
344 	private int treeFilterMarks = 0;
345 
346 	/**
347 	 * Get the old name associated with this file.
348 	 * <p>
349 	 * The meaning of the old name can differ depending on the semantic meaning
350 	 * of this patch:
351 	 * <ul>
352 	 * <li><i>file add</i>: always <code>/dev/null</code></li>
353 	 * <li><i>file modify</i>: always {@link #getNewPath()}</li>
354 	 * <li><i>file delete</i>: always the file being deleted</li>
355 	 * <li><i>file copy</i>: source file the copy originates from</li>
356 	 * <li><i>file rename</i>: source file the rename originates from</li>
357 	 * </ul>
358 	 *
359 	 * @return old name for this file.
360 	 */
361 	public String getOldPath() {
362 		return oldPath;
363 	}
364 
365 	/**
366 	 * Get the new name associated with this file.
367 	 * <p>
368 	 * The meaning of the new name can differ depending on the semantic meaning
369 	 * of this patch:
370 	 * <ul>
371 	 * <li><i>file add</i>: always the file being created</li>
372 	 * <li><i>file modify</i>: always {@link #getOldPath()}</li>
373 	 * <li><i>file delete</i>: always <code>/dev/null</code></li>
374 	 * <li><i>file copy</i>: destination file the copy ends up at</li>
375 	 * <li><i>file rename</i>: destination file the rename ends up at</li>
376 	 * </ul>
377 	 *
378 	 * @return new name for this file.
379 	 */
380 	public String getNewPath() {
381 		return newPath;
382 	}
383 
384 	/**
385 	 * Get the path associated with this file.
386 	 *
387 	 * @param side
388 	 *            which path to obtain.
389 	 * @return name for this file.
390 	 */
391 	public String getPath(Side side) {
392 		return side == Side.OLD ? getOldPath() : getNewPath();
393 	}
394 
395 	/** @return the old file mode, if described in the patch */
396 	public FileMode getOldMode() {
397 		return oldMode;
398 	}
399 
400 	/** @return the new file mode, if described in the patch */
401 	public FileMode getNewMode() {
402 		return newMode;
403 	}
404 
405 	/**
406 	 * Get the mode associated with this file.
407 	 *
408 	 * @param side
409 	 *            which mode to obtain.
410 	 * @return the mode.
411 	 */
412 	public FileMode getMode(Side side) {
413 		return side == Side.OLD ? getOldMode() : getNewMode();
414 	}
415 
416 	/** @return the type of change this patch makes on {@link #getNewPath()} */
417 	public ChangeType getChangeType() {
418 		return changeType;
419 	}
420 
421 	/**
422 	 * @return similarity score between {@link #getOldPath()} and
423 	 *         {@link #getNewPath()} if {@link #getChangeType()} is
424 	 *         {@link ChangeType#COPY} or {@link ChangeType#RENAME}.
425 	 */
426 	public int getScore() {
427 		return score;
428 	}
429 
430 	/**
431 	 * Get the old object id from the <code>index</code>.
432 	 *
433 	 * @return the object id; null if there is no index line
434 	 */
435 	public AbbreviatedObjectId getOldId() {
436 		return oldId;
437 	}
438 
439 	/**
440 	 * Get the new object id from the <code>index</code>.
441 	 *
442 	 * @return the object id; null if there is no index line
443 	 */
444 	public AbbreviatedObjectId getNewId() {
445 		return newId;
446 	}
447 
448 	/**
449 	 * Whether the mark tree filter with the specified index matched during scan
450 	 * or not, see {@link #scan(TreeWalk, boolean, TreeFilter...)}. Example:
451 	 * <p>
452 	 *
453 	 * <pre>
454 	 * TreeFilter filterA = ...;
455 	 * TreeFilter filterB = ...;
456 	 * List&lt;DiffEntry&gt; entries = DiffEntry.scan(walk, false, filterA, filterB);
457 	 * DiffEntry entry = entries.get(0);
458 	 * boolean filterAMatched = entry.isMarked(0);
459 	 * boolean filterBMatched = entry.isMarked(1);
460 	 * </pre>
461 	 * <p>
462 	 * Note that 0 corresponds to filterA because it was the first filter that
463 	 * was passed to scan.
464 	 * <p>
465 	 * To query more than one flag at once, see {@link #getTreeFilterMarks()}.
466 	 *
467 	 * @param index
468 	 *            the index of the tree filter to check for (must be between 0
469 	 *            and {@link Integer#SIZE}).
470 	 *
471 	 * @return true, if the tree filter matched; false if not
472 	 * @since 2.3
473 	 */
474 	public boolean isMarked(int index) {
475 		return (treeFilterMarks & (1L << index)) != 0;
476 	}
477 
478 	/**
479 	 * Get the raw tree filter marks, as set during
480 	 * {@link #scan(TreeWalk, boolean, TreeFilter...)}. See
481 	 * {@link #isMarked(int)} to query each mark individually.
482 	 *
483 	 * @return the bitset of tree filter marks
484 	 * @since 2.3
485 	 */
486 	public int getTreeFilterMarks() {
487 		return treeFilterMarks;
488 	}
489 
490 	/**
491 	 * Get the object id.
492 	 *
493 	 * @param side
494 	 *            the side of the id to get.
495 	 * @return the object id; null if there is no index line
496 	 */
497 	public AbbreviatedObjectId getId(Side side) {
498 		return side == Side.OLD ? getOldId() : getNewId();
499 	}
500 
501 	@SuppressWarnings("nls")
502 	@Override
503 	public String toString() {
504 		StringBuilder buf = new StringBuilder();
505 		buf.append("DiffEntry[");
506 		buf.append(changeType);
507 		buf.append(" ");
508 		switch (changeType) {
509 		case ADD:
510 			buf.append(newPath);
511 			break;
512 		case COPY:
513 			buf.append(oldPath + "->" + newPath);
514 			break;
515 		case DELETE:
516 			buf.append(oldPath);
517 			break;
518 		case MODIFY:
519 			buf.append(oldPath);
520 			break;
521 		case RENAME:
522 			buf.append(oldPath + "->" + newPath);
523 			break;
524 		}
525 		buf.append("]");
526 		return buf.toString();
527 	}
528 }