-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathPaginator.java
More file actions
239 lines (216 loc) · 6.97 KB
/
Paginator.java
File metadata and controls
239 lines (216 loc) · 6.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gitiles;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.annotation.Nullable;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RevWalkException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.FollowFilter;
import org.eclipse.jgit.revwalk.RenameCallback;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
/**
* Wrapper around {@link RevWalk} that paginates for Gitiles.
*
* <p>A single page of a shortlog is defined by a revision range, such as "master" or
* "master..next", a page size, and a start commit, such as "c0ffee". The distance between the first
* commit in the walk ("next") and the first commit in the page may be arbitrarily long, but in
* order to present the commit list in a stable way, we must always start from the first commit in
* the walk. This is because there may be arbitrary merge commits between "c0ffee" and "next" that
* effectively insert arbitrary commits into the history starting from "c0ffee".
*/
class Paginator implements Iterable<RevCommit> {
private static class RenameWatcher extends RenameCallback {
private DiffEntry entry;
@Override
public void renamed(DiffEntry entry) {
this.entry = entry;
}
private DiffEntry getAndClear() {
DiffEntry e = entry;
entry = null;
return e;
}
}
private final RevWalk walk;
private final int limit;
private final ObjectId prevStart;
private final RenameWatcher renameWatcher;
private RevCommit first;
private boolean done;
private int n;
private ObjectId nextStart;
private Map<ObjectId, DiffEntry> renamed;
/**
* Construct a paginator and walk eagerly to the first returned commit.
*
* @param walk revision walk; must be fully initialized before calling.
* @param limit page size.
* @param start commit at which to start the walk, or null to start at the beginning.
*/
Paginator(RevWalk walk, int limit, @Nullable ObjectId start)
throws MissingObjectException, IncorrectObjectTypeException, IOException {
this.walk = checkNotNull(walk, "walk");
checkArgument(limit > 0, "limit must be positive: %s", limit);
this.limit = limit;
TreeFilter filter = walk.getTreeFilter();
if (filter instanceof FollowFilter) {
renameWatcher = new RenameWatcher();
((FollowFilter) filter).setRenameCallback(renameWatcher);
} else {
renameWatcher = null;
}
Deque<ObjectId> prevBuffer = new ArrayDeque<>(start != null ? limit : 0);
while (true) {
RevCommit commit = nextWithRename();
if (commit == null) {
done = true;
break;
}
if (start == null || start.equals(commit)) {
first = commit;
break;
}
if (prevBuffer.size() == limit) {
prevBuffer.remove();
}
prevBuffer.add(commit);
}
prevStart = prevBuffer.pollFirst();
}
/**
* Get the next element in this page of the walk.
*
* @return the next element, or null if the walk is finished.
* @throws MissingObjectException See {@link RevWalk#next()}.
* @throws IncorrectObjectTypeException See {@link RevWalk#next()}.
* @throws IOException See {@link RevWalk#next()}.
*/
public @Nullable RevCommit next()
throws MissingObjectException, IncorrectObjectTypeException, IOException {
if (done) {
return null;
}
RevCommit commit;
if (first != null) {
commit = first;
first = null;
} else {
commit = nextWithRename();
}
if (++n == limit) {
nextStart = nextWithRename();
done = true;
} else if (commit == null) {
done = true;
}
return commit;
}
private RevCommit nextWithRename() throws IOException {
RevCommit next = walk.next();
if (renameWatcher != null) {
// The commit that triggered the rename isn't available to RenameWatcher,
// so we can't populate the map from the callback directly. Instead, we
// need to check after each call to walk.next() whether a rename occurred
// due to this commit.
DiffEntry entry = renameWatcher.getAndClear();
if (entry != null) {
if (renamed == null) {
renamed = new HashMap<>();
}
renamed.put(next.copy(), entry);
}
}
return next;
}
/**
* Get previous start.
*
* @return the ID at the start of the page of results preceding this one, or null if this is the
* first page.
*/
public ObjectId getPreviousStart() {
return prevStart;
}
/**
* Get next start.
*
* @return the ID at the start of the page of results after this one, or null if this is the last
* page.
*/
public ObjectId getNextStart() {
checkState(done, "getNextStart() invalid before walk done");
return nextStart;
}
/**
* Get rename.
*
* @return entry corresponding to a rename or copy at the given commit.
*/
public @Nullable DiffEntry getRename(ObjectId commitId) {
return renamed != null ? renamed.get(commitId) : null;
}
/**
* Get iterator over the commits in this walk.
*
* @return an iterator over the commits in this walk.
* @throws RevWalkException if an error occurred, wrapping the checked exception from {@link
* #next()}.
*/
@Override
public Iterator<RevCommit> iterator() {
return new Iterator<RevCommit>() {
RevCommit next = nextUnchecked();
@Override
public boolean hasNext() {
return next != null;
}
@Override
public RevCommit next() {
RevCommit r = next;
next = nextUnchecked();
return r;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public int getLimit() {
return limit;
}
public RevWalk getWalk() {
return walk;
}
private RevCommit nextUnchecked() {
try {
return next();
} catch (IOException e) {
throw new RevWalkException(e);
}
}
}