blob: 120b9af847cf6ada39bc91beca9fd914c8394740 [file] [log] [blame]
// Copyright (C) 2008 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.google.gerrit.httpd.rpc.changedetail;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.changedetail.RebaseChange;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Creates a {@link ChangeDetail} from a {@link Change}. */
public class ChangeDetailFactory extends Handler<ChangeDetail> {
public interface Factory {
ChangeDetailFactory create(Change.Id id);
}
private final ChangeControl.Factory changeControlFactory;
private final PatchSetDetailFactory.Factory patchSetDetail;
private final AccountInfoCacheFactory aic;
private final AnonymousUser anonymousUser;
private final ReviewDb db;
private final GitRepositoryManager repoManager;
private final Change.Id changeId;
private ChangeDetail detail;
private ChangeControl control;
private Map<PatchSet.Id, PatchSet> patchsetsById;
private final MergeOp.Factory opFactory;
private boolean testMerge;
private List<PatchSetAncestor> currentPatchSetAncestors;
private List<PatchSet> currentDepPatchSets;
private List<Change> currentDepChanges;
@Inject
ChangeDetailFactory(
final PatchSetDetailFactory.Factory patchSetDetail, final ReviewDb db,
final GitRepositoryManager repoManager,
final ChangeControl.Factory changeControlFactory,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
final AnonymousUser anonymousUser,
final MergeOp.Factory opFactory,
@GerritServerConfig final Config cfg,
@Assisted final Change.Id id) {
this.patchSetDetail = patchSetDetail;
this.db = db;
this.repoManager = repoManager;
this.changeControlFactory = changeControlFactory;
this.anonymousUser = anonymousUser;
this.aic = accountInfoCacheFactory.create();
this.opFactory = opFactory;
this.testMerge = cfg.getBoolean("changeMerge", "test", false);
this.changeId = id;
}
@Override
public ChangeDetail call() throws OrmException, NoSuchEntityException,
PatchSetInfoNotAvailableException, NoSuchChangeException,
RepositoryNotFoundException, IOException, NoSuchProjectException {
control = changeControlFactory.validateFor(changeId);
final Change change = control.getChange();
final PatchSet patch = db.patchSets().get(change.currentPatchSetId());
if (patch == null) {
throw new NoSuchEntityException();
}
aic.want(change.getOwner());
detail = new ChangeDetail();
detail.setChange(change);
detail.setAllowsAnonymous(control.forUser(anonymousUser).isVisible(db));
detail.setCanAbandon(change.getStatus() != Change.Status.DRAFT && change.getStatus().isOpen() && control.canAbandon());
detail.setCanPublish(control.canPublish(db));
detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED
&& control.canRestore()
&& ProjectUtil.branchExists(repoManager, change.getDest()));
detail.setCanDeleteDraft(control.canDeleteDraft(db));
detail.setStarred(control.getCurrentUser().getStarredChanges().contains(
changeId));
detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
detail.setCanEdit(control.getRefControl().canWrite());
detail.setCanEditCommitMessage(change.getStatus().isOpen() && control.canAddPatchSet());
detail.setCanEditTopicName(control.canEditTopicName());
List<SubmitRecord> submitRecords = control.getSubmitRecords(db, patch);
for (SubmitRecord rec : submitRecords) {
if (rec.labels != null) {
for (SubmitRecord.Label lbl : rec.labels) {
aic.want(lbl.appliedBy);
}
}
if (detail.getChange().getStatus().isOpen()
&& rec.status == SubmitRecord.Status.OK
&& control.getRefControl().canSubmit()
&& ProjectUtil.branchExists(repoManager, change.getDest())) {
detail.setCanSubmit(true);
}
}
detail.setSubmitRecords(submitRecords);
detail.setSubmitTypeRecord(control.getSubmitTypeRecord(db, patch));
patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
loadPatchSets();
loadMessages();
if (change.currentPatchSetId() != null) {
loadCurrentPatchSet();
}
load();
detail.setCanRebase(detail.getChange().getStatus().isOpen() &&
control.canRebase() &&
RebaseChange.canDoRebase(db, change, repoManager,
currentPatchSetAncestors, currentDepPatchSets, currentDepChanges));
detail.setAccounts(aic.create());
return detail;
}
private void loadPatchSets() throws OrmException {
ResultSet<PatchSet> source = db.patchSets().byChange(changeId);
List<PatchSet> patches = new ArrayList<PatchSet>();
for (PatchSet ps : source) {
if (control.isPatchVisible(ps, db)) {
patches.add(ps);
}
patchsetsById.put(ps.getId(), ps);
}
detail.setPatchSets(patches);
}
private void loadMessages() throws OrmException {
ResultSet<ChangeMessage> source = db.changeMessages().byChange(changeId);
List<ChangeMessage> msgList = new ArrayList<ChangeMessage>();
for (ChangeMessage msg : source) {
PatchSet.Id id = msg.getPatchSetId();
if (id != null) {
PatchSet ps = patchsetsById.get(msg.getPatchSetId());
if (control.isPatchVisible(ps, db)) {
msgList.add(msg);
}
} else {
// Not guaranteed to have a non-null patchset id, so just display it.
msgList.add(msg);
}
}
detail.setMessages(msgList);
for (final ChangeMessage m : detail.getMessages()) {
aic.want(m.getAuthor());
}
}
private void load() throws OrmException, NoSuchChangeException,
NoSuchProjectException {
final Change.Status status = detail.getChange().getStatus();
if ((status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) &&
testMerge) {
ChangeUtil.testMerge(opFactory, detail.getChange());
}
}
private boolean isReviewer(Change change) {
// Return true if the currently logged in user is a reviewer of the change.
try {
return control.isReviewer(db, new ChangeData(change));
} catch (OrmException e) {
return false;
}
}
private void loadCurrentPatchSet() throws OrmException,
NoSuchEntityException, PatchSetInfoNotAvailableException,
NoSuchChangeException {
currentDepPatchSets = new ArrayList<PatchSet>();
currentDepChanges = new ArrayList<Change>();
final PatchSet currentPatch = findCurrentOrLatestPatchSet();
final PatchSet.Id psId = currentPatch.getId();
final PatchSetDetailFactory loader = patchSetDetail.create(null, psId, null);
loader.patchSet = currentPatch;
loader.control = control;
detail.setCurrentPatchSetDetail(loader.call());
detail.setCurrentPatchSetId(psId);
final HashSet<Change.Id> changesToGet = new HashSet<Change.Id>();
final HashMap<Change.Id,PatchSet.Id> ancestorPatchIds =
new HashMap<Change.Id,PatchSet.Id>();
final List<Change.Id> ancestorOrder = new ArrayList<Change.Id>();
currentPatchSetAncestors = db.patchSetAncestors().ancestorsOf(psId).toList();
for (PatchSetAncestor a : currentPatchSetAncestors) {
for (PatchSet p : db.patchSets().byRevision(a.getAncestorRevision())) {
currentDepPatchSets.add(p);
final Change.Id ck = p.getId().getParentKey();
if (changesToGet.add(ck)) {
ancestorPatchIds.put(ck, p.getId());
ancestorOrder.add(ck);
}
}
}
final Set<PatchSet.Id> descendants = new HashSet<PatchSet.Id>();
RevId cprev;
for (PatchSet p : detail.getPatchSets()) {
cprev = p.getRevision();
if (cprev != null) {
for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(cprev)) {
if (descendants.add(a.getPatchSet())) {
changesToGet.add(a.getPatchSet().getParentKey());
}
}
}
}
final Map<Change.Id, Change> m =
db.changes().toMap(db.changes().get(changesToGet));
final CurrentUser currentUser = control.getCurrentUser();
Account.Id currentUserId = null;
if (currentUser instanceof IdentifiedUser) {
currentUserId = ((IdentifiedUser) currentUser).getAccountId();
}
final ArrayList<ChangeInfo> dependsOn = new ArrayList<ChangeInfo>();
for (final Change.Id a : ancestorOrder) {
final Change ac = m.get(a);
if (ac != null && ac.getProject().equals(detail.getChange().getProject())) {
currentDepChanges.add(ac);
if (ac.getStatus().getCode() != Change.STATUS_DRAFT
|| ac.getOwner().equals(currentUserId)
|| isReviewer(ac)) {
dependsOn.add(newChangeInfo(ac, ancestorPatchIds));
}
}
}
final ArrayList<ChangeInfo> neededBy = new ArrayList<ChangeInfo>();
for (final PatchSet.Id a : descendants) {
final Change ac = m.get(a.getParentKey());
if (ac != null && ac.currentPatchSetId().equals(a)) {
if (ac.getStatus().getCode() != Change.STATUS_DRAFT
|| ac.getOwner().equals(currentUserId)
|| isReviewer(ac)) {
neededBy.add(newChangeInfo(ac, null));
}
}
}
Collections.sort(neededBy, new Comparator<ChangeInfo>() {
public int compare(final ChangeInfo o1, final ChangeInfo o2) {
return o1.getId().get() - o2.getId().get();
}
});
detail.setDependsOn(dependsOn);
detail.setNeededBy(neededBy);
}
private PatchSet findCurrentOrLatestPatchSet() {
PatchSet currentPatch = detail.getCurrentPatchSet();
// If the current patch set is a draft and user can't see it, set the
// current patch set to whatever the latest one is
if (currentPatch == null) {
List<PatchSet> patchSets = detail.getPatchSets();
if (!detail.getPatchSets().isEmpty()) {
currentPatch = patchSets.get(patchSets.size() - 1);
} else {
// Shouldn't happen, change shouldn't be visible if all the patchsets
// are drafts
}
}
return currentPatch;
}
private ChangeInfo newChangeInfo(final Change ac,
Map<Change.Id,PatchSet.Id> ancestorPatchIds) {
aic.want(ac.getOwner());
ChangeInfo ci;
if (ancestorPatchIds == null) {
ci = new ChangeInfo(ac);
} else {
ci = new ChangeInfo(ac, ancestorPatchIds.get(ac.getId()));
}
ci.setStarred(isStarred(ac));
return ci;
}
private boolean isStarred(final Change ac) {
return control.getCurrentUser().getStarredChanges().contains(ac.getId());
}
}