| // 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()); |
| } |
| } |