blob: 019d33cfd3d0533cac4966fe0a7cefdb03081e15 [file] [log] [blame]
Dave Borowitz9de65952012-08-13 16:09:45 -07001// Copyright 2012 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles;
16
17import static com.google.common.base.Preconditions.checkArgument;
18import static com.google.common.base.Preconditions.checkNotNull;
19import static com.google.common.base.Preconditions.checkState;
20
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040021import org.eclipse.jgit.diff.DiffEntry;
Dave Borowitz9de65952012-08-13 16:09:45 -070022import org.eclipse.jgit.errors.IncorrectObjectTypeException;
23import org.eclipse.jgit.errors.MissingObjectException;
24import org.eclipse.jgit.errors.RevWalkException;
25import org.eclipse.jgit.lib.ObjectId;
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040026import org.eclipse.jgit.revwalk.FollowFilter;
27import org.eclipse.jgit.revwalk.RenameCallback;
Dave Borowitz9de65952012-08-13 16:09:45 -070028import org.eclipse.jgit.revwalk.RevCommit;
29import org.eclipse.jgit.revwalk.RevWalk;
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040030import org.eclipse.jgit.treewalk.filter.TreeFilter;
Dave Borowitz9de65952012-08-13 16:09:45 -070031
32import java.io.IOException;
33import java.util.ArrayDeque;
34import java.util.Deque;
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040035import java.util.HashMap;
Dave Borowitz9de65952012-08-13 16:09:45 -070036import java.util.Iterator;
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040037import java.util.Map;
Dave Borowitz9de65952012-08-13 16:09:45 -070038
39import javax.annotation.Nullable;
40
41/**
42 * Wrapper around {@link RevWalk} that paginates for Gitiles.
43 *
44 * A single page of a shortlog is defined by a revision range, such as "master"
45 * or "master..next", a page size, and a start commit, such as "c0ffee". The
46 * distance between the first commit in the walk ("next") and the first commit
47 * in the page may be arbitrarily long, but in order to present the commit list
48 * in a stable way, we must always start from the first commit in the walk. This
49 * is because there may be arbitrary merge commits between "c0ffee" and "next"
50 * that effectively insert arbitrary commits into the history starting from
51 * "c0ffee".
52 */
53class Paginator implements Iterable<RevCommit> {
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040054 private static class RenameWatcher extends RenameCallback {
55 private DiffEntry entry;
56
57 @Override
58 public void renamed(DiffEntry entry) {
59 this.entry = entry;
60 }
61
62 private DiffEntry getAndClear() {
63 DiffEntry e = entry;
64 entry = null;
65 return e;
66 }
67 }
68
Dave Borowitz9de65952012-08-13 16:09:45 -070069 private final RevWalk walk;
Dave Borowitz9de65952012-08-13 16:09:45 -070070 private final int limit;
Dave Borowitz7756efb2014-07-30 08:45:20 -070071 private final ObjectId prevStart;
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040072 private final RenameWatcher renameWatcher;
Dave Borowitz9de65952012-08-13 16:09:45 -070073
Dave Borowitz7756efb2014-07-30 08:45:20 -070074 private RevCommit first;
Dave Borowitz9de65952012-08-13 16:09:45 -070075 private boolean done;
Dave Borowitz9de65952012-08-13 16:09:45 -070076 private int n;
Dave Borowitz9de65952012-08-13 16:09:45 -070077 private ObjectId nextStart;
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040078 private Map<ObjectId, DiffEntry> renamed;
Dave Borowitz9de65952012-08-13 16:09:45 -070079
80 /**
Dave Borowitz7756efb2014-07-30 08:45:20 -070081 * Construct a paginator and walk eagerly to the first returned commit.
82 *
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040083 * @param walk revision walk; must be fully initialized before calling.
Dave Borowitz9de65952012-08-13 16:09:45 -070084 * @param limit page size.
85 * @param start commit at which to start the walk, or null to start at the
86 * beginning.
87 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020088 Paginator(RevWalk walk, int limit, @Nullable ObjectId start)
89 throws MissingObjectException, IncorrectObjectTypeException, IOException {
Dave Borowitz9de65952012-08-13 16:09:45 -070090 this.walk = checkNotNull(walk, "walk");
Dave Borowitz9de65952012-08-13 16:09:45 -070091 checkArgument(limit > 0, "limit must be positive: %s", limit);
92 this.limit = limit;
Dave Borowitz7756efb2014-07-30 08:45:20 -070093
Dave Borowitzcb88d4d2015-10-26 13:58:17 -040094 TreeFilter filter = walk.getTreeFilter();
95 if (filter instanceof FollowFilter) {
96 renameWatcher = new RenameWatcher();
97 ((FollowFilter) filter).setRenameCallback(renameWatcher);
98 } else {
99 renameWatcher = null;
100 }
Dave Borowitz7756efb2014-07-30 08:45:20 -0700101
Dave Borowitz96a6f472014-11-04 16:38:20 -0800102 Deque<ObjectId> prevBuffer = new ArrayDeque<>(start != null ? limit : 0);
Dave Borowitz7756efb2014-07-30 08:45:20 -0700103 while (true) {
Dave Borowitzcb88d4d2015-10-26 13:58:17 -0400104 RevCommit commit = nextWithRename();
Dave Borowitz7756efb2014-07-30 08:45:20 -0700105 if (commit == null) {
106 done = true;
107 break;
108 }
109 if (start == null || start.equals(commit)) {
110 first = commit;
111 break;
112 }
113 if (prevBuffer.size() == limit) {
114 prevBuffer.remove();
115 }
116 prevBuffer.add(commit);
117 }
118 prevStart = prevBuffer.pollFirst();
Dave Borowitz9de65952012-08-13 16:09:45 -0700119 }
120
121 /**
122 * Get the next element in this page of the walk.
123 *
124 * @return the next element, or null if the walk is finished.
125 *
126 * @throws MissingObjectException See {@link RevWalk#next()}.
127 * @throws IncorrectObjectTypeException See {@link RevWalk#next()}.
128 * @throws IOException See {@link RevWalk#next()}.
129 */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200130 public RevCommit next() throws MissingObjectException, IncorrectObjectTypeException, IOException {
Dave Borowitz7756efb2014-07-30 08:45:20 -0700131 if (done) {
132 return null;
133 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700134 RevCommit commit;
Dave Borowitz7756efb2014-07-30 08:45:20 -0700135 if (first != null) {
136 commit = first;
137 first = null;
Dave Borowitz9de65952012-08-13 16:09:45 -0700138 } else {
Dave Borowitzcb88d4d2015-10-26 13:58:17 -0400139 commit = nextWithRename();
Dave Borowitz9de65952012-08-13 16:09:45 -0700140 }
Dave Borowitz9de65952012-08-13 16:09:45 -0700141 if (++n == limit) {
Dave Borowitzcb88d4d2015-10-26 13:58:17 -0400142 nextStart = nextWithRename();
Dave Borowitz9de65952012-08-13 16:09:45 -0700143 done = true;
Dave Borowitz7756efb2014-07-30 08:45:20 -0700144 } else if (commit == null) {
Dave Borowitz9de65952012-08-13 16:09:45 -0700145 done = true;
Dave Borowitz9de65952012-08-13 16:09:45 -0700146 }
147 return commit;
148 }
149
Dave Borowitzcb88d4d2015-10-26 13:58:17 -0400150 private RevCommit nextWithRename() throws IOException {
151 RevCommit next = walk.next();
152 if (renameWatcher != null) {
153 // The commit that triggered the rename isn't available to RenameWatcher,
154 // so we can't populate the map from the callback directly. Instead, we
155 // need to check after each call to walk.next() whether a rename occurred
156 // due to this commit.
157 DiffEntry entry = renameWatcher.getAndClear();
158 if (entry != null) {
159 if (renamed == null) {
160 renamed = new HashMap<>();
161 }
162 renamed.put(next.copy(), entry);
163 }
164 }
165 return next;
166 }
167
Dave Borowitz9de65952012-08-13 16:09:45 -0700168 /**
169 * @return the ID at the start of the page of results preceding this one, or
170 * null if this is the first page.
171 */
172 public ObjectId getPreviousStart() {
Dave Borowitz7756efb2014-07-30 08:45:20 -0700173 return prevStart;
Dave Borowitz9de65952012-08-13 16:09:45 -0700174 }
175
176 /**
177 * @return the ID at the start of the page of results after this one, or null
178 * if this is the last page.
179 */
180 public ObjectId getNextStart() {
181 checkState(done, "getNextStart() invalid before walk done");
182 return nextStart;
183 }
184
185 /**
Dave Borowitzcb88d4d2015-10-26 13:58:17 -0400186 * @return entry corresponding to a rename or copy at the given commit.
187 */
188 public DiffEntry getRename(ObjectId commitId) {
189 return renamed != null ? renamed.get(commitId) : null;
190 }
191
192 /**
Dave Borowitz9de65952012-08-13 16:09:45 -0700193 * @return an iterator over the commits in this walk.
194 * @throws RevWalkException if an error occurred, wrapping the checked
195 * exception from {@link #next()}.
196 */
197 @Override
198 public Iterator<RevCommit> iterator() {
199 return new Iterator<RevCommit>() {
200 RevCommit next = nextUnchecked();
201
202 @Override
203 public boolean hasNext() {
204 return next != null;
205 }
206
207 @Override
208 public RevCommit next() {
209 RevCommit r = next;
210 next = nextUnchecked();
211 return r;
212 }
213
214 @Override
215 public void remove() {
216 throw new UnsupportedOperationException();
217 }
218 };
219 }
220
Dave Borowitz27fada42013-11-01 11:09:49 -0700221 public int getLimit() {
222 return limit;
223 }
224
225 public RevWalk getWalk() {
226 return walk;
227 }
228
Dave Borowitz9de65952012-08-13 16:09:45 -0700229 private RevCommit nextUnchecked() {
230 try {
231 return next();
Dave Borowitz9de65952012-08-13 16:09:45 -0700232 } catch (IOException e) {
233 throw new RevWalkException(e);
234 }
235 }
236}