Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project
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 :
15 : package com.google.gerrit.server.change;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 :
19 : import com.google.auto.value.AutoValue;
20 : import com.google.common.annotations.VisibleForTesting;
21 : import com.google.common.cache.Cache;
22 : import com.google.common.cache.Weigher;
23 : import com.google.common.collect.FluentIterable;
24 : import com.google.common.flogger.FluentLogger;
25 : import com.google.gerrit.common.Nullable;
26 : import com.google.gerrit.entities.Change;
27 : import com.google.gerrit.entities.PatchSet;
28 : import com.google.gerrit.entities.Project;
29 : import com.google.gerrit.exceptions.StorageException;
30 : import com.google.gerrit.extensions.client.ChangeKind;
31 : import com.google.gerrit.proto.Protos;
32 : import com.google.gerrit.server.cache.CacheModule;
33 : import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
34 : import com.google.gerrit.server.cache.serialize.CacheSerializer;
35 : import com.google.gerrit.server.cache.serialize.EnumCacheSerializer;
36 : import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
37 : import com.google.gerrit.server.config.GerritServerConfig;
38 : import com.google.gerrit.server.git.GitRepositoryManager;
39 : import com.google.gerrit.server.git.InMemoryInserter;
40 : import com.google.gerrit.server.git.MergeUtil;
41 : import com.google.gerrit.server.query.change.ChangeData;
42 : import com.google.inject.Inject;
43 : import com.google.inject.Module;
44 : import com.google.inject.name.Named;
45 : import java.io.IOException;
46 : import java.util.Arrays;
47 : import java.util.Collection;
48 : import java.util.Objects;
49 : import java.util.Set;
50 : import java.util.concurrent.Callable;
51 : import java.util.concurrent.ExecutionException;
52 : import org.eclipse.jgit.errors.LargeObjectException;
53 : import org.eclipse.jgit.lib.AnyObjectId;
54 : import org.eclipse.jgit.lib.Config;
55 : import org.eclipse.jgit.lib.ObjectId;
56 : import org.eclipse.jgit.lib.ObjectInserter;
57 : import org.eclipse.jgit.lib.Repository;
58 : import org.eclipse.jgit.merge.ThreeWayMerger;
59 : import org.eclipse.jgit.revwalk.RevCommit;
60 : import org.eclipse.jgit.revwalk.RevWalk;
61 :
62 : public class ChangeKindCacheImpl implements ChangeKindCache {
63 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
64 :
65 : private static final String ID_CACHE = "change_kind";
66 :
67 : public static Module module() {
68 152 : return new CacheModule() {
69 : @Override
70 : protected void configure() {
71 152 : bind(ChangeKindCache.class).to(ChangeKindCacheImpl.class);
72 152 : persist(ID_CACHE, Key.class, ChangeKind.class)
73 152 : .maximumWeight(2 << 20)
74 152 : .weigher(ChangeKindWeigher.class)
75 152 : .version(1)
76 152 : .keySerializer(new Key.Serializer())
77 152 : .valueSerializer(new EnumCacheSerializer<>(ChangeKind.class));
78 152 : }
79 : };
80 : }
81 :
82 : public static class NoCache implements ChangeKindCache {
83 : private final boolean useRecursiveMerge;
84 : private final ChangeData.Factory changeDataFactory;
85 : private final GitRepositoryManager repoManager;
86 :
87 : @Inject
88 : NoCache(
89 : @GerritServerConfig Config serverConfig,
90 : ChangeData.Factory changeDataFactory,
91 0 : GitRepositoryManager repoManager) {
92 0 : this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
93 0 : this.changeDataFactory = changeDataFactory;
94 0 : this.repoManager = repoManager;
95 0 : }
96 :
97 : @Override
98 : public ChangeKind getChangeKind(
99 : Project.NameKey project,
100 : @Nullable RevWalk rw,
101 : @Nullable Config repoConfig,
102 : ObjectId prior,
103 : ObjectId next) {
104 : try {
105 0 : Key key = Key.create(prior, next, useRecursiveMerge);
106 0 : return new Loader(key, repoManager, project, rw, repoConfig).call();
107 0 : } catch (IOException e) {
108 0 : logger.atWarning().withCause(e).log(
109 0 : "Cannot check trivial rebase of new patch set %s in %s", next.name(), project);
110 0 : return ChangeKind.REWORK;
111 : }
112 : }
113 :
114 : @Override
115 : public ChangeKind getChangeKind(Change change, PatchSet patch) {
116 0 : return getChangeKindInternal(this, change, patch, changeDataFactory, repoManager);
117 : }
118 :
119 : @Override
120 : public ChangeKind getChangeKind(
121 : @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) {
122 0 : return getChangeKindInternal(this, rw, repoConfig, cd, patch);
123 : }
124 : }
125 :
126 : @AutoValue
127 54 : public abstract static class Key {
128 : public static Key create(AnyObjectId prior, AnyObjectId next, String strategyName) {
129 54 : return new AutoValue_ChangeKindCacheImpl_Key(prior.copy(), next.copy(), strategyName);
130 : }
131 :
132 : private static Key create(AnyObjectId prior, AnyObjectId next, boolean useRecursiveMerge) {
133 53 : return create(prior, next, MergeUtil.mergeStrategyName(true, useRecursiveMerge));
134 : }
135 :
136 : public abstract ObjectId prior();
137 :
138 : public abstract ObjectId next();
139 :
140 : public abstract String strategyName();
141 :
142 : @VisibleForTesting
143 152 : static class Serializer implements CacheSerializer<Key> {
144 : @Override
145 : public byte[] serialize(Key object) {
146 2 : ObjectIdConverter idConverter = ObjectIdConverter.create();
147 2 : return Protos.toByteArray(
148 2 : ChangeKindKeyProto.newBuilder()
149 2 : .setPrior(idConverter.toByteString(object.prior()))
150 2 : .setNext(idConverter.toByteString(object.next()))
151 2 : .setStrategyName(object.strategyName())
152 2 : .build());
153 : }
154 :
155 : @Override
156 : public Key deserialize(byte[] in) {
157 1 : ChangeKindKeyProto proto = Protos.parseUnchecked(ChangeKindKeyProto.parser(), in);
158 1 : ObjectIdConverter idConverter = ObjectIdConverter.create();
159 1 : return create(
160 1 : idConverter.fromByteString(proto.getPrior()),
161 1 : idConverter.fromByteString(proto.getNext()),
162 1 : proto.getStrategyName());
163 : }
164 : }
165 : }
166 :
167 : private static class Loader implements Callable<ChangeKind> {
168 : private final Key key;
169 : private final GitRepositoryManager repoManager;
170 : private final Project.NameKey projectName;
171 : private final RevWalk alreadyOpenRw;
172 : private final Config repoConfig;
173 :
174 : private Loader(
175 : Key key,
176 : GitRepositoryManager repoManager,
177 : Project.NameKey projectName,
178 : @Nullable RevWalk rw,
179 53 : @Nullable Config repoConfig) {
180 53 : checkArgument(
181 : (rw == null && repoConfig == null) || (rw != null && repoConfig != null),
182 : "must either provide both revwalk/config, or neither; got %s/%s",
183 : rw,
184 : repoConfig);
185 53 : this.key = key;
186 53 : this.repoManager = repoManager;
187 53 : this.projectName = projectName;
188 53 : this.alreadyOpenRw = rw;
189 53 : this.repoConfig = repoConfig;
190 53 : }
191 :
192 : @SuppressWarnings("resource") // Resources are manually managed.
193 : @Override
194 : public ChangeKind call() throws IOException {
195 53 : if (Objects.equals(key.prior(), key.next())) {
196 2 : return ChangeKind.NO_CODE_CHANGE;
197 : }
198 :
199 53 : RevWalk rw = alreadyOpenRw;
200 53 : Config config = repoConfig;
201 53 : Repository repo = null;
202 53 : if (alreadyOpenRw == null) {
203 3 : repo = repoManager.openRepository(projectName);
204 3 : rw = new RevWalk(repo);
205 3 : config = repo.getConfig();
206 : }
207 : try {
208 53 : RevCommit prior = rw.parseCommit(key.prior());
209 53 : rw.parseBody(prior);
210 53 : RevCommit next = rw.parseCommit(key.next());
211 53 : rw.parseBody(next);
212 :
213 53 : if (!next.getFullMessage().equals(prior.getFullMessage())) {
214 37 : if (isSameDeltaAndTree(rw, prior, next)) {
215 24 : return ChangeKind.NO_CODE_CHANGE;
216 : }
217 32 : return ChangeKind.REWORK;
218 : }
219 :
220 45 : if (isSameDeltaAndTree(rw, prior, next)) {
221 15 : return ChangeKind.NO_CHANGE;
222 : }
223 :
224 44 : if (prior.getParentCount() == 0 || next.getParentCount() == 0) {
225 : // At this point we have considered all the kinds that could be applicable to root
226 : // commits; the remainder of the checks in this method all assume that both commits have
227 : // at least one parent.
228 9 : return ChangeKind.REWORK;
229 : }
230 :
231 43 : if ((prior.getParentCount() > 1 || next.getParentCount() > 1)
232 11 : && !onlyFirstParentChanged(prior, next)) {
233 : // Trivial rebases done by machine only work well on 1 parent.
234 11 : return ChangeKind.REWORK;
235 : }
236 :
237 : // A trivial rebase can be detected by looking for the next commit
238 : // having the same tree as would exist when the prior commit is
239 : // cherry-picked onto the next commit's new first parent.
240 41 : try (ObjectInserter ins = new InMemoryInserter(rw.getObjectReader())) {
241 41 : ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName());
242 41 : merger.setBase(prior.getParent(0));
243 41 : if (merger.merge(next.getParent(0), prior)
244 41 : && merger.getResultTreeId().equals(next.getTree())) {
245 15 : if (prior.getParentCount() == 1) {
246 15 : return ChangeKind.TRIVIAL_REBASE;
247 : }
248 1 : return ChangeKind.MERGE_FIRST_PARENT_UPDATE;
249 : }
250 15 : } catch (LargeObjectException e) {
251 : // Some object is too large for the merge attempt to succeed. Assume
252 : // it was a rework.
253 37 : }
254 37 : return ChangeKind.REWORK;
255 : } finally {
256 53 : if (repo != null) {
257 3 : rw.close();
258 3 : repo.close();
259 : }
260 : }
261 : }
262 :
263 : public static boolean onlyFirstParentChanged(RevCommit prior, RevCommit next) {
264 11 : return !sameFirstParents(prior, next) && sameRestOfParents(prior, next);
265 : }
266 :
267 : private static boolean sameFirstParents(RevCommit prior, RevCommit next) {
268 11 : if (prior.getParentCount() == 0) {
269 0 : return next.getParentCount() == 0;
270 : }
271 11 : return prior.getParent(0).equals(next.getParent(0));
272 : }
273 :
274 : private static boolean sameRestOfParents(RevCommit prior, RevCommit next) {
275 3 : Set<RevCommit> priorRestParents = allExceptFirstParent(prior.getParents());
276 3 : Set<RevCommit> nextRestParents = allExceptFirstParent(next.getParents());
277 3 : return priorRestParents.equals(nextRestParents);
278 : }
279 :
280 : private static Set<RevCommit> allExceptFirstParent(RevCommit[] parents) {
281 3 : return FluentIterable.from(Arrays.asList(parents)).skip(1).toSet();
282 : }
283 :
284 : private static boolean isSameDeltaAndTree(RevWalk rw, RevCommit prior, RevCommit next)
285 : throws IOException {
286 53 : if (!Objects.equals(next.getTree(), prior.getTree())) {
287 52 : return false;
288 : }
289 :
290 28 : if (prior.getParentCount() != next.getParentCount()) {
291 8 : return false;
292 27 : } else if (prior.getParentCount() == 0) {
293 1 : return true;
294 : }
295 :
296 : // Make sure that the prior/next delta is the same - not just the tree.
297 : // This is done by making sure that the parent trees are equal.
298 26 : for (int i = 0; i < prior.getParentCount(); i++) {
299 : // Parse parent commits so that their trees are available.
300 26 : rw.parseCommit(prior.getParent(i));
301 26 : rw.parseCommit(next.getParent(i));
302 :
303 26 : if (!Objects.equals(next.getParent(i).getTree(), prior.getParent(i).getTree())) {
304 7 : return false;
305 : }
306 : }
307 25 : return true;
308 : }
309 : }
310 :
311 152 : public static class ChangeKindWeigher implements Weigher<Key, ChangeKind> {
312 : @Override
313 : public int weigh(Key key, ChangeKind changeKind) {
314 53 : return 16
315 : + 2 * 36
316 53 : + 2 * key.strategyName().length() // Size of Key, 64 bit JVM
317 53 : + 2 * changeKind.name().length(); // Size of ChangeKind, 64 bit JVM
318 : }
319 : }
320 :
321 : private final Cache<Key, ChangeKind> cache;
322 : private final boolean useRecursiveMerge;
323 : private final ChangeData.Factory changeDataFactory;
324 : private final GitRepositoryManager repoManager;
325 :
326 : @Inject
327 : ChangeKindCacheImpl(
328 : @GerritServerConfig Config serverConfig,
329 : @Named(ID_CACHE) Cache<Key, ChangeKind> cache,
330 : ChangeData.Factory changeDataFactory,
331 146 : GitRepositoryManager repoManager) {
332 146 : this.cache = cache;
333 146 : this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
334 146 : this.changeDataFactory = changeDataFactory;
335 146 : this.repoManager = repoManager;
336 146 : }
337 :
338 : @Override
339 : public ChangeKind getChangeKind(
340 : Project.NameKey project,
341 : @Nullable RevWalk rw,
342 : @Nullable Config repoConfig,
343 : ObjectId prior,
344 : ObjectId next) {
345 : try {
346 53 : Key key = Key.create(prior, next, useRecursiveMerge);
347 53 : ChangeKind kind = cache.get(key, new Loader(key, repoManager, project, rw, repoConfig));
348 53 : logger.atFine().log("Change kind of new patch set %s in %s: %s", next.name(), project, kind);
349 53 : return kind;
350 1 : } catch (ExecutionException e) {
351 1 : logger.atWarning().withCause(e).log(
352 1 : "Cannot check change kind of new patch set %s in %s", next.name(), project);
353 1 : return ChangeKind.REWORK;
354 : }
355 : }
356 :
357 : @Override
358 : public ChangeKind getChangeKind(Change change, PatchSet patch) {
359 2 : return getChangeKindInternal(this, change, patch, changeDataFactory, repoManager);
360 : }
361 :
362 : @Override
363 : public ChangeKind getChangeKind(
364 : @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) {
365 103 : return getChangeKindInternal(this, rw, repoConfig, cd, patch);
366 : }
367 :
368 : private static ChangeKind getChangeKindInternal(
369 : ChangeKindCache cache,
370 : @Nullable RevWalk rw,
371 : @Nullable Config repoConfig,
372 : ChangeData change,
373 : PatchSet patch) {
374 103 : ChangeKind kind = ChangeKind.REWORK;
375 : // Trivial case: if we're on the first patch, we don't need to use
376 : // the repository.
377 103 : if (patch.id().get() > 1) {
378 : try {
379 52 : Collection<PatchSet> patchSetCollection = change.patchSets();
380 52 : PatchSet priorPs = patch;
381 52 : for (PatchSet ps : patchSetCollection) {
382 52 : if (ps.id().get() < patch.id().get()
383 52 : && (ps.id().get() > priorPs.id().get() || priorPs == patch)) {
384 : // We only want the previous patch set, so walk until the last one
385 52 : priorPs = ps;
386 : }
387 52 : }
388 :
389 : // If we still think the previous patch is the current patch,
390 : // we only have one patch set. Return the default.
391 : // This can happen if a user creates a draft, uploads a second patch,
392 : // and deletes the draft.
393 52 : if (priorPs != patch) {
394 52 : kind =
395 52 : cache.getChangeKind(
396 52 : change.project(), rw, repoConfig, priorPs.commitId(), patch.commitId());
397 : }
398 0 : } catch (StorageException e) {
399 : // Do nothing; assume we have a complex change
400 0 : logger.atWarning().withCause(e).log(
401 : "Unable to get change kind for patchSet %s of change %s",
402 0 : patch.number(), change.getId());
403 52 : }
404 : }
405 103 : logger.atFine().log(
406 103 : "Change kind for patchSet %s of change %s: %s", patch.number(), change.getId(), kind);
407 103 : return kind;
408 : }
409 :
410 : private static ChangeKind getChangeKindInternal(
411 : ChangeKindCache cache,
412 : Change change,
413 : PatchSet patch,
414 : ChangeData.Factory changeDataFactory,
415 : GitRepositoryManager repoManager) {
416 : // TODO - dborowitz: add NEW_CHANGE type for default.
417 2 : ChangeKind kind = ChangeKind.REWORK;
418 : // Trivial case: if we're on the first patch, we don't need to open
419 : // the repository.
420 2 : if (patch.id().get() > 1) {
421 2 : try (Repository repo = repoManager.openRepository(change.getProject());
422 2 : RevWalk rw = new RevWalk(repo)) {
423 2 : kind =
424 2 : getChangeKindInternal(
425 2 : cache, rw, repo.getConfig(), changeDataFactory.create(change), patch);
426 0 : } catch (IOException e) {
427 : // Do nothing; assume we have a complex change
428 0 : logger.atWarning().withCause(e).log(
429 : "Unable to get change kind for patchSet %s of change %s",
430 0 : patch.number(), change.getChangeId());
431 2 : }
432 : }
433 2 : logger.atFine().log(
434 2 : "Change kind for patchSet %s of change %s: %s", patch.number(), change.getChangeId(), kind);
435 2 : return kind;
436 : }
437 : }
|