blob: d6a7213d6f86c0df088ad5f3bec276b1fc0181ff [file] [log] [blame]
// Copyright (C) 2012 The Android Open Source Project
//
// 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.googlesource.gerrit.plugins.github.git;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Id;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.MergeException;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.ssh.NoSshInfo;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.util.ChangeIdUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
public class PullRequestCreateChange {
private static final Logger LOG = LoggerFactory
.getLogger(PullRequestCreateChange.class);
private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
private final IdentifiedUser currentUser;
private final CommitValidators.Factory commitValidatorsFactory;
private final ChangeInserter.Factory changeInserterFactory;
private final PatchSetInserter.Factory patchSetInserterFactory;
private final ProjectControl.Factory projectControlFactory;
private final GenericFactory userFactory;
private final Provider<InternalChangeQuery> queryProvider;
@Inject
PullRequestCreateChange(IdentifiedUser currentUser,
CommitValidators.Factory commitValidatorsFactory,
ChangeInserter.Factory changeInserterFactory,
PatchSetInserter.Factory patchSetInserterFactory,
ProjectControl.Factory projectControlFactory,
IdentifiedUser.GenericFactory userFactory,
Provider<InternalChangeQuery> queryProvider) {
this.currentUser = currentUser;
this.commitValidatorsFactory = commitValidatorsFactory;
this.changeInserterFactory = changeInserterFactory;
this.patchSetInserterFactory = patchSetInserterFactory;
this.projectControlFactory = projectControlFactory;
this.userFactory = userFactory;
this.queryProvider = queryProvider;
}
public Change.Id addCommitToChange(final ReviewDb db, final Project project,
final Repository git, final String destinationBranch,
final Account.Id pullRequestOwner, final RevCommit pullRequestCommit,
final String pullRequestMesage, final String topic, boolean doValidation)
throws NoSuchChangeException, EmailException, OrmException,
MissingObjectException, IncorrectObjectTypeException, IOException,
InvalidChangeOperationException, MergeException, NoSuchProjectException {
Id newChange = null;
if (destinationBranch == null || destinationBranch.length() == 0) {
throw new InvalidChangeOperationException(
"Destination branch cannot be null or empty");
}
RefControl refControl =
projectControlFactory.controlFor(project.getNameKey()).controlForRef(
destinationBranch);
try (RevWalk revWalk = new RevWalk(git)) {
try {
Ref destRef = git.getRef(destinationBranch);
if (destRef == null) {
throw new InvalidChangeOperationException("Branch "
+ destinationBranch + " does not exist.");
}
String pullRequestSha1 = pullRequestCommit.getId().getName();
ResultSet<PatchSet> existingPatchSet =
db.patchSets().byRevision(new RevId(pullRequestSha1));
Iterator<PatchSet> patchSetIterator = existingPatchSet.iterator();
if (patchSetIterator.hasNext()) {
PatchSet patchSet = patchSetIterator.next();
LOG.debug("Pull request commit ID " + pullRequestSha1
+ " has been already uploaded as PatchSetID="
+ patchSet.getPatchSetId() + " in ChangeID=" + patchSet.getId());
return null;
}
Change.Key changeKey;
final List<String> idList = pullRequestCommit.getFooterLines(CHANGE_ID);
if (!idList.isEmpty()) {
final String idStr = idList.get(idList.size() - 1).trim();
changeKey = new Change.Key(idStr);
} else {
final ObjectId computedChangeId =
ChangeIdUtil.computeChangeId(pullRequestCommit.getTree(),
pullRequestCommit, pullRequestCommit.getAuthorIdent(),
pullRequestCommit.getCommitterIdent(), pullRequestMesage);
changeKey = new Change.Key("I" + computedChangeId.name());
}
List<ChangeData> destChanges =
queryProvider.get().byBranchKey(
new Branch.NameKey(project.getNameKey(), destRef.getName()),
changeKey);
if (destChanges.size() > 1) {
throw new InvalidChangeOperationException(
"Multiple Changes with Change-ID "
+ changeKey
+ " already exist on the target branch: cannot add a new patch-set "
+ destinationBranch);
} else if (destChanges.size() == 1) {
// The change key exists on the destination branch: adding a new
// patch-set
Change destChange = destChanges.get(0).change();
ChangeControl changeControl =
projectControlFactory.controlFor(project.getNameKey()).controlFor(
destChange).forUser(userFactory.create(pullRequestOwner));
return insertPatchSet(git, revWalk, destChange, pullRequestCommit,
changeControl, pullRequestOwner, pullRequestMesage, doValidation);
} else {
// Change key not found on destination branch. We can create a new
// change.
return (newChange =
createNewChange(db, git, revWalk, changeKey,
project.getNameKey(), destRef, pullRequestOwner,
pullRequestCommit, refControl, pullRequestMesage, topic,
doValidation));
}
} finally {
if (newChange == null) {
db.rollback();
}
}
} finally {
git.close();
}
}
private Change.Id insertPatchSet(Repository git, RevWalk revWalk,
Change change, RevCommit cherryPickCommit, ChangeControl changeControl,
Account.Id pullRequestOwnerId, String pullRequestMessage,
boolean doValidation) throws InvalidChangeOperationException,
IOException, OrmException, NoSuchChangeException {
PatchSetInserter patchSetInserter =
patchSetInserterFactory.create(git, revWalk, changeControl, cherryPickCommit);
// This apparently useless method call is made for triggering
// the creation of patchSet inside PatchSetInserter and thus avoiding a NPE
patchSetInserter.getPatchSetId();
patchSetInserter.setMessage(pullRequestMessage);
patchSetInserter.setValidatePolicy(doValidation ? ValidatePolicy.GERRIT
: ValidatePolicy.NONE);
patchSetInserter.insert();
return change.getId();
}
private Change.Id createNewChange(ReviewDb db, Repository git,
RevWalk revWalk, Change.Key changeKey, Project.NameKey project,
Ref destRef, Account.Id pullRequestOwner, RevCommit pullRequestCommit,
RefControl refControl, String pullRequestMessage, String topic,
boolean doValidation) throws OrmException,
InvalidChangeOperationException, IOException {
Change change =
new Change(changeKey, new Change.Id(db.nextChangeId()),
pullRequestOwner, new Branch.NameKey(project, destRef.getName()),
TimeUtil.nowTs());
if (topic != null) {
change.setTopic(topic);
}
ChangeInserter ins = changeInserterFactory.create(
refControl.getProjectControl(), change, pullRequestCommit);
PatchSet newPatchSet = ins.getPatchSet();
if (doValidation) {
validate(git, pullRequestCommit, refControl, newPatchSet);
}
final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
ru.setExpectedOldObjectId(ObjectId.zeroId());
ru.setNewObjectId(pullRequestCommit);
ru.disableRefLog();
if (ru.update(revWalk) != RefUpdate.Result.NEW) {
throw new IOException(String.format("Failed to create ref %s in %s: %s",
newPatchSet.getRefName(), change.getDest().getParentKey().get(),
ru.getResult()));
}
ins.setMessage(
buildChangeMessage(db, change, newPatchSet, pullRequestOwner, pullRequestMessage))
.insert();
return change.getId();
}
private void validate(Repository git, RevCommit pullRequestCommit,
RefControl refControl, PatchSet newPatchSet)
throws InvalidChangeOperationException {
CommitValidators commitValidators =
commitValidatorsFactory.create(refControl, new NoSshInfo(), git);
CommitReceivedEvent commitReceivedEvent =
new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
pullRequestCommit.getId(), newPatchSet.getRefName()), refControl
.getProjectControl().getProject(), refControl.getRefName(),
pullRequestCommit, currentUser);
try {
commitValidators.validateForGerritCommits(commitReceivedEvent);
} catch (CommitValidationException e) {
throw new InvalidChangeOperationException(e.getMessage());
}
}
private ChangeMessage buildChangeMessage(ReviewDb db, Change dest,
PatchSet newPatchSet, Account.Id pullRequestAuthorId,
String pullRequestMessage) throws OrmException {
ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(dest.getId(),
ChangeUtil.messageUUID(db)), pullRequestAuthorId, TimeUtil.nowTs(),
newPatchSet.getId());
cmsg.setMessage(pullRequestMessage);
return cmsg;
}
}