blob: 62dfb5556da6ff33534719299b89217e945f767c [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
21import org.eclipse.jgit.errors.IncorrectObjectTypeException;
22import org.eclipse.jgit.errors.MissingObjectException;
23import org.eclipse.jgit.errors.RevWalkException;
24import org.eclipse.jgit.lib.ObjectId;
25import org.eclipse.jgit.revwalk.RevCommit;
26import org.eclipse.jgit.revwalk.RevWalk;
27
28import java.io.IOException;
29import java.util.ArrayDeque;
30import java.util.Deque;
31import java.util.Iterator;
32
33import javax.annotation.Nullable;
34
35/**
36 * Wrapper around {@link RevWalk} that paginates for Gitiles.
37 *
38 * A single page of a shortlog is defined by a revision range, such as "master"
39 * or "master..next", a page size, and a start commit, such as "c0ffee". The
40 * distance between the first commit in the walk ("next") and the first commit
41 * in the page may be arbitrarily long, but in order to present the commit list
42 * in a stable way, we must always start from the first commit in the walk. This
43 * is because there may be arbitrary merge commits between "c0ffee" and "next"
44 * that effectively insert arbitrary commits into the history starting from
45 * "c0ffee".
46 */
47class Paginator implements Iterable<RevCommit> {
48 private final RevWalk walk;
49 private final ObjectId start;
50 private final int limit;
51 private final Deque<ObjectId> prevBuffer;
52
53 private boolean done;
54 private int i;
55 private int n;
56 private int foundIndex;
57 private ObjectId nextStart;
58
59 /**
60 * @param walk revision walk.
61 * @param limit page size.
62 * @param start commit at which to start the walk, or null to start at the
63 * beginning.
64 */
65 Paginator(RevWalk walk, int limit, @Nullable ObjectId start) {
66 this.walk = checkNotNull(walk, "walk");
67 this.start = start;
68 checkArgument(limit > 0, "limit must be positive: %s", limit);
69 this.limit = limit;
70 prevBuffer = new ArrayDeque<ObjectId>(start != null ? limit : 0);
71 i = -1;
72 foundIndex = -1;
73 }
74
75 /**
76 * Get the next element in this page of the walk.
77 *
78 * @return the next element, or null if the walk is finished.
79 *
80 * @throws MissingObjectException See {@link RevWalk#next()}.
81 * @throws IncorrectObjectTypeException See {@link RevWalk#next()}.
82 * @throws IOException See {@link RevWalk#next()}.
83 */
84 public RevCommit next() throws MissingObjectException, IncorrectObjectTypeException,
85 IOException {
86 RevCommit commit;
87 if (foundIndex < 0) {
88 while (true) {
89 commit = walk.next();
90 if (commit == null) {
91 done = true;
92 return null;
93 }
94 i++;
95 if (start == null || start.equals(commit)) {
96 foundIndex = i;
97 break;
98 }
99 if (prevBuffer.size() == limit) {
100 prevBuffer.remove();
101 }
102 prevBuffer.add(commit);
103 }
104 } else {
105 commit = walk.next();
106 }
107
108 if (++n == limit) {
109 done = true;
110 } else if (n == limit + 1 || commit == null) {
111 nextStart = commit;
112 done = true;
113 return null;
114 }
115 return commit;
116 }
117
118 /**
119 * @return the ID at the start of the page of results preceding this one, or
120 * null if this is the first page.
121 */
122 public ObjectId getPreviousStart() {
123 checkState(done, "getPreviousStart() invalid before walk done");
124 return prevBuffer.pollFirst();
125 }
126
127 /**
128 * @return the ID at the start of the page of results after this one, or null
129 * if this is the last page.
130 */
131 public ObjectId getNextStart() {
132 checkState(done, "getNextStart() invalid before walk done");
133 return nextStart;
134 }
135
136 /**
137 * @return an iterator over the commits in this walk.
138 * @throws RevWalkException if an error occurred, wrapping the checked
139 * exception from {@link #next()}.
140 */
141 @Override
142 public Iterator<RevCommit> iterator() {
143 return new Iterator<RevCommit>() {
144 RevCommit next = nextUnchecked();
145
146 @Override
147 public boolean hasNext() {
148 return next != null;
149 }
150
151 @Override
152 public RevCommit next() {
153 RevCommit r = next;
154 next = nextUnchecked();
155 return r;
156 }
157
158 @Override
159 public void remove() {
160 throw new UnsupportedOperationException();
161 }
162 };
163 }
164
Dave Borowitz27fada42013-11-01 11:09:49 -0700165 public int getLimit() {
166 return limit;
167 }
168
169 public RevWalk getWalk() {
170 return walk;
171 }
172
Dave Borowitz9de65952012-08-13 16:09:45 -0700173 private RevCommit nextUnchecked() {
174 try {
175 return next();
176 } catch (MissingObjectException e) {
177 throw new RevWalkException(e);
178 } catch (IncorrectObjectTypeException e) {
179 throw new RevWalkException(e);
180 } catch (IOException e) {
181 throw new RevWalkException(e);
182 }
183 }
184}