blob: 102b71d7a6eba5b84d0983d0f05f82352c489901 [file] [log] [blame]
// Copyright 2014 Criteo
//
// 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.criteo.gerrit.plugins.automerge;
import com.google.common.collect.Lists;
import com.google.gerrit.common.ChangeListener;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ListChangesOption;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.change.ChangesCollection;
import com.google.gerrit.server.change.GetRelated;
import com.google.gerrit.server.change.PostReview;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.Submit;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.events.ChangeEvent;
import com.google.gerrit.server.events.CommentAddedEvent;
import com.google.gerrit.server.events.PatchSetCreatedEvent;
import com.google.gerrit.server.events.TopicChangedEvent;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import java.io.IOException;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
/**
* Starts at the same time as the gerrit server, and sets up our change hook
* listener.
*/
public class AutomaticMerger implements ChangeListener, LifecycleListener {
@Inject
private GerritApi api;
private final AtomicityHelper atomicityHelper;
@Inject
private AccountByEmailCache byEmailCache;
@Inject
ChangeData.Factory changeDataFactory;
@Inject
private ChangeControl.GenericFactory changeFactory;
@Inject
private ChangesCollection collection;
private final AutomergeConfig config;
@Inject
Provider<ReviewDb> db;
@Inject
private IdentifiedUser.GenericFactory factory;
@Inject
GetRelated getRelated;
@Inject
MergeUtil.Factory mergeUtilFactory;
@Inject
Provider<PostReview> reviewer;
private final ReviewUpdater reviewUpdater;
@Inject
Submit submitter;
@Inject
public AutomaticMerger(@GerritServerConfig final Config gerritConfig) {
config = new AutomergeConfig(gerritConfig);
reviewUpdater = new ReviewUpdater(config);
atomicityHelper = new AtomicityHelper(config);
}
@Override
synchronized public void onChangeEvent(final ChangeEvent event) {
try {
if (event instanceof TopicChangedEvent) {
final TopicChangedEvent newComment = (TopicChangedEvent) event;
final ChangeAttribute change = newComment.change;
final int reviewNumber = Integer.parseInt(change.number);
try {
api.changes().id(reviewNumber).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
} catch (final RestApiException e1) {
throw new RuntimeException(e1);
}
if (atomicityHelper.isAtomicReview(change)) {
if (atomicityHelper.hasDependentReview(reviewNumber)) {
reviewUpdater.setMinusTwo(reviewNumber, AutomergeConfig.ATOMIC_REVIEWS_SAME_REPO_FILE);
} else {
reviewUpdater.commentOnReview(reviewNumber, AutomergeConfig.ATOMIC_REVIEW_DETECTED_FILE);
}
}
}
if (event instanceof PatchSetCreatedEvent) {
final PatchSetCreatedEvent newComment = (PatchSetCreatedEvent) event;
final ChangeAttribute change = newComment.change;
final int reviewNumber = Integer.parseInt(change.number);
try {
api.changes().id(reviewNumber).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
} catch (final RestApiException e1) {
throw new RuntimeException(e1);
}
if (atomicityHelper.isAtomicReview(change)) {
if (atomicityHelper.hasDependentReview(reviewNumber)) {
reviewUpdater.setMinusTwo(reviewNumber, AutomergeConfig.ATOMIC_REVIEWS_SAME_REPO_FILE);
} else {
reviewUpdater.commentOnReview(reviewNumber, AutomergeConfig.ATOMIC_REVIEW_DETECTED_FILE);
}
}
}
} catch (NoSuchChangeException | OrmException | IOException | AuthException | BadRequestException
| UnprocessableEntityException e) {
throw new RuntimeException(e);
}
if (event instanceof CommentAddedEvent) {
final CommentAddedEvent newComment = (CommentAddedEvent) event;
final ChangeAttribute change = newComment.change;
final int reviewNumber = Integer.parseInt(change.number);
try {
api.changes().id(reviewNumber).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
} catch (final RestApiException e1) {
throw new RuntimeException(e1);
}
if (newComment.author.email.contains("qabot")) {
return;
}
final String topic = change.topic;
try {
if (atomicityHelper.isSubmittable(Integer.parseInt(newComment.change.number))) {
final List<ChangeInfo> related = Lists.newArrayList();
if (topic != null) {
related.addAll(api.changes().query("status: open AND topic: " + topic)
.withOption(ListChangesOption.CURRENT_REVISION).get());
} else {
related.add(api.changes().id(change.id).get(EnumSet.of(ListChangesOption.CURRENT_REVISION)));
}
boolean mergeable = true;
String why = null;
for (final ChangeInfo info : related) {
api.changes().id(change.id).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
if (!info.mergeable) {
mergeable = false;
why = String.format("Review %s is approved but not mergeable.", info._number);
}
if (!atomicityHelper.isSubmittable(info._number)) {
mergeable = false;
// why = String.format("Review %s is not approved.",
// info._number);
}
}
if (mergeable) {
for (final ChangeInfo info : related) {
final SubmitInput input = new SubmitInput();
input.waitForMerge = true;
final Set<Account.Id> ids = byEmailCache.get(config.getBotEmail());
final IdentifiedUser bot = factory.create(ids.iterator().next());
final ChangeControl ctl = changeFactory.controlFor(new Change.Id(info._number), bot);
final ChangeData changeData = changeDataFactory.create(db.get(), new Change.Id(info._number));
final RevisionResource r = new RevisionResource(collection.parse(ctl), changeData.currentPatchSet());
submitter.apply(r, input);
}
} else {
if (why != null) {
reviewUpdater.commentOnReview(reviewNumber, AutomergeConfig.CANT_MERGE_COMMENT_FILE);
}
}
}
} catch (final RestApiException e) {
throw new RuntimeException(e);
} catch (final OrmException e) {
throw new RuntimeException(e);
} catch (final NumberFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (final NoSuchChangeException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (final RepositoryNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (final IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Override
public void start() {
}
@Override
public void stop() {
}
}