View Javadoc
1   /*
2    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> 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  
11  package org.eclipse.jgit.revwalk;
12  
13  import java.io.IOException;
14  import java.util.List;
15  
16  import org.eclipse.jgit.diff.DiffConfig;
17  import org.eclipse.jgit.diff.DiffEntry;
18  import org.eclipse.jgit.diff.DiffEntry.ChangeType;
19  import org.eclipse.jgit.diff.RenameDetector;
20  import org.eclipse.jgit.errors.CorruptObjectException;
21  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
22  import org.eclipse.jgit.errors.MissingObjectException;
23  import org.eclipse.jgit.errors.StopWalkException;
24  import org.eclipse.jgit.lib.ObjectId;
25  import org.eclipse.jgit.revwalk.filter.RevFilter;
26  import org.eclipse.jgit.treewalk.TreeWalk;
27  import org.eclipse.jgit.treewalk.filter.TreeFilter;
28  
29  /**
30   * Filter applying a {@link org.eclipse.jgit.treewalk.filter.TreeFilter} against
31   * changed paths in each commit.
32   * <p>
33   * Each commit is differenced concurrently against all of its parents to look
34   * for tree entries that are interesting to the
35   * {@link org.eclipse.jgit.treewalk.filter.TreeFilter}.
36   *
37   * @since 3.5
38   */
39  public class TreeRevFilter extends RevFilter {
40  	private static final int PARSED = RevWalk.PARSED;
41  
42  	private static final int UNINTERESTING = RevWalk.UNINTERESTING;
43  
44  	private final int rewriteFlag;
45  	private final TreeWalk pathFilter;
46  
47  	/**
48  	 * Create a {@link org.eclipse.jgit.revwalk.filter.RevFilter} from a
49  	 * {@link org.eclipse.jgit.treewalk.filter.TreeFilter}.
50  	 *
51  	 * @param walker
52  	 *            walker used for reading trees.
53  	 * @param t
54  	 *            filter to compare against any changed paths in each commit. If
55  	 *            a {@link org.eclipse.jgit.revwalk.FollowFilter}, will be
56  	 *            replaced with a new filter following new paths after a rename.
57  	 * @since 3.5
58  	 */
59  	public TreeRevFilter(RevWalk walker, TreeFilter t) {
60  		this(walker, t, 0);
61  	}
62  
63  
64  	/**
65  	 * Create a filter for the first phase of a parent-rewriting limited revision
66  	 * walk.
67  	 * <p>
68  	 * This filter is ANDed to evaluate before all other filters and ties the
69  	 * configured {@link TreeFilter} into the revision walking process.
70  	 * <p>
71  	 * If no interesting tree entries are found the commit is colored with
72  	 * {@code rewriteFlag}, allowing a later pass implemented by
73  	 * {@link RewriteGenerator} to remove those colored commits from the DAG.
74  	 *
75  	 * @see RewriteGenerator
76  	 *
77  	 * @param walker
78  	 *            walker used for reading trees.
79  	 * @param t
80  	 *            filter to compare against any changed paths in each commit. If a
81  	 *            {@link FollowFilter}, will be replaced with a new filter
82  	 *            following new paths after a rename.
83  	 * @param rewriteFlag
84  	 *            flag to color commits to be removed from the simplified DAT.
85  	 */
86  	TreeRevFilter(RevWalk walker, TreeFilter t, int rewriteFlag) {
87  		pathFilter = new TreeWalk(walker.reader);
88  		pathFilter.setFilter(t);
89  		pathFilter.setRecursive(t.shouldBeRecursive());
90  		this.rewriteFlag = rewriteFlag;
91  	}
92  
93  	/** {@inheritDoc} */
94  	@Override
95  	public RevFilter clone() {
96  		throw new UnsupportedOperationException();
97  	}
98  
99  	/** {@inheritDoc} */
100 	@Override
101 	public boolean include(RevWalk walker, RevCommit c)
102 			throws StopWalkException, MissingObjectException,
103 			IncorrectObjectTypeException, IOException {
104 		// Reset the tree filter to scan this commit and parents.
105 		//
106 		RevCommit[] pList = c.parents;
107 		int nParents = pList.length;
108 		TreeWalk tw = pathFilter;
109 		ObjectId[] trees = new ObjectId[nParents + 1];
110 		for (int i = 0; i < nParents; i++) {
111 			RevCommit p = c.parents[i];
112 			if ((p.flags & PARSED) == 0) {
113 				p.parseHeaders(walker);
114 			}
115 			trees[i] = p.getTree();
116 		}
117 		trees[nParents] = c.getTree();
118 		tw.reset(trees);
119 
120 		if (nParents == 1) {
121 			// We have exactly one parent. This is a very common case.
122 			//
123 			int chgs = 0, adds = 0;
124 			while (tw.next()) {
125 				chgs++;
126 				if (tw.getRawMode(0) == 0 && tw.getRawMode(1) != 0) {
127 					adds++;
128 				} else {
129 					break; // no point in looking at this further.
130 				}
131 			}
132 
133 			if (chgs == 0) {
134 				// No changes, so our tree is effectively the same as
135 				// our parent tree. We pass the buck to our parent.
136 				//
137 				c.flags |= rewriteFlag;
138 				return false;
139 			}
140 
141 			// We have interesting items, but neither of the special
142 			// cases denoted above.
143 			//
144 			if (adds > 0 && tw.getFilter() instanceof FollowFilter) {
145 				// One of the paths we care about was added in this
146 				// commit. We need to update our filter to its older
147 				// name, if we can discover it. Find out what that is.
148 				//
149 				updateFollowFilter(trees, ((FollowFilter) tw.getFilter()).cfg);
150 			}
151 			return true;
152 		} else if (nParents == 0) {
153 			// We have no parents to compare against. Consider us to be
154 			// REWRITE only if we have no paths matching our filter.
155 			//
156 			if (tw.next()) {
157 				return true;
158 			}
159 			c.flags |= rewriteFlag;
160 			return false;
161 		}
162 
163 		// We are a merge commit. We can only be REWRITE if we are same
164 		// to _all_ parents. We may also be able to eliminate a parent if
165 		// it does not contribute changes to us. Such a parent may be an
166 		// uninteresting side branch.
167 		//
168 		int[] chgs = new int[nParents];
169 		int[] adds = new int[nParents];
170 		while (tw.next()) {
171 			int myMode = tw.getRawMode(nParents);
172 			for (int i = 0; i < nParents; i++) {
173 				int pMode = tw.getRawMode(i);
174 				if (myMode == pMode && tw.idEqual(i, nParents)) {
175 					continue;
176 				}
177 				chgs[i]++;
178 				if (pMode == 0 && myMode != 0) {
179 					adds[i]++;
180 				}
181 			}
182 		}
183 
184 		boolean same = false;
185 		boolean diff = false;
186 		for (int i = 0; i < nParents; i++) {
187 			if (chgs[i] == 0) {
188 				// No changes, so our tree is effectively the same as
189 				// this parent tree. We pass the buck to only this one
190 				// parent commit.
191 				//
192 
193 				RevCommit p = pList[i];
194 				if ((p.flags & UNINTERESTING) != 0) {
195 					// This parent was marked as not interesting by the
196 					// application. We should look for another parent
197 					// that is interesting.
198 					//
199 					same = true;
200 					continue;
201 				}
202 
203 				c.flags |= rewriteFlag;
204 				c.parents = new RevCommit[] { p };
205 				return false;
206 			}
207 
208 			if (chgs[i] == adds[i]) {
209 				// All of the differences from this parent were because we
210 				// added files that they did not have. This parent is our
211 				// "empty tree root" and thus their history is not relevant.
212 				// Cut our grandparents to be an empty list.
213 				//
214 				pList[i].parents = RevCommit.NO_PARENTS;
215 			}
216 
217 			// We have an interesting difference relative to this parent.
218 			//
219 			diff = true;
220 		}
221 
222 		if (diff && !same) {
223 			// We did not abort above, so we are different in at least one
224 			// way from all of our parents. We have to take the blame for
225 			// that difference.
226 			//
227 			return true;
228 		}
229 
230 		// We are the same as all of our parents. We must keep them
231 		// as they are and allow those parents to flow into pending
232 		// for further scanning.
233 		//
234 		c.flags |= rewriteFlag;
235 		return false;
236 	}
237 
238 	/** {@inheritDoc} */
239 	@Override
240 	public boolean requiresCommitBody() {
241 		return false;
242 	}
243 
244 	private void updateFollowFilter(ObjectId[] trees, DiffConfig cfg)
245 			throws MissingObjectException, IncorrectObjectTypeException,
246 			CorruptObjectException, IOException {
247 		TreeWalk tw = pathFilter;
248 		FollowFilter oldFilter = (FollowFilter) tw.getFilter();
249 		tw.setFilter(TreeFilter.ANY_DIFF);
250 		tw.reset(trees);
251 
252 		List<DiffEntry> files = DiffEntry.scan(tw);
253 		RenameDetector rd = new RenameDetector(tw.getObjectReader(), cfg);
254 		rd.addAll(files);
255 		files = rd.compute();
256 
257 		TreeFilter newFilter = oldFilter;
258 		for (DiffEntry ent : files) {
259 			if (isRename(ent) && ent.getNewPath().equals(oldFilter.getPath())) {
260 				newFilter = FollowFilter.create(ent.getOldPath(), cfg);
261 				RenameCallback callback = oldFilter.getRenameCallback();
262 				if (callback != null) {
263 					callback.renamed(ent);
264 					// forward the callback to the new follow filter
265 					((FollowFilter) newFilter).setRenameCallback(callback);
266 				}
267 				break;
268 			}
269 		}
270 		tw.setFilter(newFilter);
271 	}
272 
273 	private static boolean isRename(DiffEntry ent) {
274 		return ent.getChangeType() == ChangeType.RENAME
275 				|| ent.getChangeType() == ChangeType.COPY;
276 	}
277 }