| // Copyright (C) 2009 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.server; |
| |
| import com.google.common.collect.Sets; |
| import com.google.gerrit.common.ChangeHookRunner; |
| import com.google.gerrit.common.ChangeHooks; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.Change.Status; |
| 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.PatchSetApproval; |
| import com.google.gerrit.reviewdb.client.PatchSetInfo; |
| import com.google.gerrit.reviewdb.client.RevId; |
| import com.google.gerrit.reviewdb.client.TrackingId; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.server.config.TrackingFooter; |
| import com.google.gerrit.server.config.TrackingFooters; |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.MergeOp; |
| import com.google.gerrit.server.mail.EmailException; |
| import com.google.gerrit.server.mail.RebasedPatchSetSender; |
| import com.google.gerrit.server.mail.ReplacePatchSetSender; |
| import com.google.gerrit.server.mail.ReplyToChangeSender; |
| import com.google.gerrit.server.mail.RevertedSender; |
| import com.google.gerrit.server.patch.PatchSetInfoFactory; |
| import com.google.gerrit.server.project.ChangeControl; |
| import com.google.gerrit.server.project.InvalidChangeOperationException; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.gwtorm.server.AtomicUpdate; |
| import com.google.gwtorm.server.OrmConcurrencyException; |
| import com.google.gwtorm.server.OrmException; |
| |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
| import org.eclipse.jgit.errors.MissingObjectException; |
| import org.eclipse.jgit.errors.RepositoryNotFoundException; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.merge.MergeStrategy; |
| import org.eclipse.jgit.merge.ThreeWayMerger; |
| import org.eclipse.jgit.revwalk.FooterLine; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.util.Base64; |
| import org.eclipse.jgit.util.ChangeIdUtil; |
| import org.eclipse.jgit.util.NB; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| |
| public class ChangeUtil { |
| |
| private static final Logger log = LoggerFactory.getLogger(ChangeUtil.class); |
| |
| private static int uuidPrefix; |
| private static int uuidSeq; |
| |
| /** |
| * Generate a new unique identifier for change message entities. |
| * |
| * @param db the database connection, used to increment the change message |
| * allocation sequence. |
| * @return the new unique identifier. |
| * @throws OrmException the database couldn't be incremented. |
| */ |
| public static String messageUUID(final ReviewDb db) throws OrmException { |
| final byte[] raw = new byte[8]; |
| fill(raw, db); |
| return Base64.encodeBytes(raw); |
| } |
| |
| private static synchronized void fill(byte[] raw, ReviewDb db) |
| throws OrmException { |
| if (uuidSeq == 0) { |
| uuidPrefix = db.nextChangeMessageId(); |
| uuidSeq = Integer.MAX_VALUE; |
| } |
| NB.encodeInt32(raw, 0, uuidPrefix); |
| NB.encodeInt32(raw, 4, uuidSeq--); |
| } |
| |
| public static void touch(final Change change, ReviewDb db) |
| throws OrmException { |
| try { |
| updated(change); |
| db.changes().update(Collections.singleton(change)); |
| } catch (OrmConcurrencyException e) { |
| // Ignore a concurrent update, we just wanted to tag it as newer. |
| } |
| } |
| |
| public static void updated(final Change c) { |
| c.resetLastUpdatedOn(); |
| computeSortKey(c); |
| } |
| |
| public static void updateTrackingIds(ReviewDb db, Change change, |
| TrackingFooters trackingFooters, List<FooterLine> footerLines) |
| throws OrmException { |
| if (trackingFooters.getTrackingFooters().isEmpty() || footerLines.isEmpty()) { |
| return; |
| } |
| |
| final Set<TrackingId> want = new HashSet<TrackingId>(); |
| final Set<TrackingId> have = new HashSet<TrackingId>( // |
| db.trackingIds().byChange(change.getId()).toList()); |
| |
| for (final TrackingFooter footer : trackingFooters.getTrackingFooters()) { |
| for (final FooterLine footerLine : footerLines) { |
| if (footerLine.matches(footer.footerKey())) { |
| // supporting multiple tracking-ids on a single line |
| final Matcher m = footer.match().matcher(footerLine.getValue()); |
| while (m.find()) { |
| if (m.group().isEmpty()) { |
| continue; |
| } |
| |
| String idstr; |
| if (m.groupCount() > 0) { |
| idstr = m.group(1); |
| } else { |
| idstr = m.group(); |
| } |
| |
| if (idstr.isEmpty()) { |
| continue; |
| } |
| if (idstr.length() > TrackingId.TRACKING_ID_MAX_CHAR) { |
| continue; |
| } |
| |
| want.add(new TrackingId(change.getId(), idstr, footer.system())); |
| } |
| } |
| } |
| } |
| |
| // Only insert the rows we don't have, and delete rows we don't match. |
| // |
| final Set<TrackingId> toInsert = new HashSet<TrackingId>(want); |
| final Set<TrackingId> toDelete = new HashSet<TrackingId>(have); |
| |
| toInsert.removeAll(have); |
| toDelete.removeAll(want); |
| |
| db.trackingIds().insert(toInsert); |
| db.trackingIds().delete(toDelete); |
| } |
| |
| public static void testMerge(MergeOp.Factory opFactory, Change change) { |
| opFactory.create(change.getDest()).verifyMergeability(change); |
| } |
| |
| public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src) |
| throws OrmException { |
| final int cnt = src.getParentCount(); |
| List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt); |
| for (int p = 0; p < cnt; p++) { |
| PatchSetAncestor a = |
| new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1)); |
| a.setAncestorRevision(new RevId(src.getParent(p).getId().getName())); |
| toInsert.add(a); |
| } |
| db.patchSetAncestors().insert(toInsert); |
| } |
| |
| /** |
| * Rebases a commit |
| * |
| * @param git Repository to find commits in |
| * @param inserter inserter to handle new trees and blobs. |
| * @param original The commit to rebase |
| * @param base Base to rebase against |
| * @return CommitBuilder the newly rebased commit |
| * @throws IOException Merged failed |
| */ |
| public static CommitBuilder rebaseCommit(Repository git, |
| final ObjectInserter inserter, RevCommit original, RevCommit base, |
| PersonIdent committerIdent) throws IOException { |
| |
| if (original.getParentCount() == 0) { |
| throw new IOException( |
| "Commits with no parents cannot be rebased (is this the initial commit?)."); |
| } |
| |
| if (original.getParentCount() > 1) { |
| throw new IOException( |
| "Patch sets with multiple parents cannot be rebased (merge commits)." |
| + " Parents: " + Arrays.toString(original.getParents())); |
| } |
| |
| final RevCommit parentCommit = original.getParent(0); |
| |
| if (base.equals(parentCommit)) { |
| throw new IOException("Change is already up to date."); |
| } |
| |
| final ThreeWayMerger merger = MergeStrategy.RESOLVE.newMerger(git, true); |
| merger.setObjectInserter(new ObjectInserter.Filter() { |
| @Override |
| protected ObjectInserter delegate() { |
| return inserter; |
| } |
| |
| @Override |
| public void flush() { |
| } |
| |
| @Override |
| public void release() { |
| } |
| }); |
| merger.setBase(parentCommit); |
| merger.merge(original, base); |
| |
| if (merger.getResultTreeId() == null) { |
| throw new IOException( |
| "The rebase failed since conflicts occured during the merge."); |
| } |
| |
| final CommitBuilder rebasedCommitBuilder = new CommitBuilder(); |
| |
| rebasedCommitBuilder.setTreeId(merger.getResultTreeId()); |
| rebasedCommitBuilder.setParentId(base); |
| rebasedCommitBuilder.setAuthor(original.getAuthorIdent()); |
| rebasedCommitBuilder.setMessage(original.getFullMessage()); |
| rebasedCommitBuilder.setCommitter(committerIdent); |
| |
| return rebasedCommitBuilder; |
| } |
| |
| public static void rebaseChange(final PatchSet.Id patchSetId, |
| final IdentifiedUser user, final ReviewDb db, |
| RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory, |
| final ChangeHookRunner hooks, GitRepositoryManager gitManager, |
| final PatchSetInfoFactory patchSetInfoFactory, |
| final GitReferenceUpdated replication, PersonIdent myIdent, |
| final ChangeControl.Factory changeControlFactory, |
| final ApprovalsUtil approvalsUtil) throws NoSuchChangeException, |
| EmailException, OrmException, MissingObjectException, |
| IncorrectObjectTypeException, IOException, |
| InvalidChangeOperationException { |
| |
| final Change.Id changeId = patchSetId.getParentKey(); |
| final ChangeControl changeControl = |
| changeControlFactory.validateFor(changeId); |
| |
| if (!changeControl.canRebase()) { |
| throw new InvalidChangeOperationException( |
| "Cannot rebase: New patch sets are not allowed to be added to change: " |
| + changeId.toString()); |
| } |
| |
| Change change = changeControl.getChange(); |
| final Repository git = gitManager.openRepository(change.getProject()); |
| try { |
| final RevWalk revWalk = new RevWalk(git); |
| try { |
| final PatchSet originalPatchSet = db.patchSets().get(patchSetId); |
| RevCommit branchTipCommit = null; |
| |
| List<PatchSetAncestor> patchSetAncestors = |
| db.patchSetAncestors().ancestorsOf(patchSetId).toList(); |
| if (patchSetAncestors.size() > 1) { |
| throw new IOException( |
| "The patch set you are trying to rebase is dependent on several other patch sets: " |
| + patchSetAncestors.toString()); |
| } |
| if (patchSetAncestors.size() == 1) { |
| List<PatchSet> depPatchSetList = db.patchSets() |
| .byRevision(patchSetAncestors.get(0).getAncestorRevision()) |
| .toList(); |
| if (!depPatchSetList.isEmpty()) { |
| PatchSet depPatchSet = depPatchSetList.get(0); |
| |
| Change.Id depChangeId = depPatchSet.getId().getParentKey(); |
| Change depChange = db.changes().get(depChangeId); |
| |
| if (depChange.getStatus() == Status.ABANDONED) { |
| throw new IOException("Cannot rebase against an abandoned change: " |
| + depChange.getKey().toString()); |
| } |
| if (depChange.getStatus().isOpen()) { |
| PatchSet latestDepPatchSet = |
| db.patchSets().get(depChange.currentPatchSetId()); |
| if (!depPatchSet.getId().equals(depChange.currentPatchSetId())) { |
| branchTipCommit = |
| revWalk.parseCommit(ObjectId |
| .fromString(latestDepPatchSet.getRevision().get())); |
| } else { |
| throw new IOException( |
| "Change is already based on the latest patch set of the dependent change."); |
| } |
| } |
| } |
| } |
| |
| if (branchTipCommit == null) { |
| // We are dependent on a merged PatchSet or have no PatchSet |
| // dependencies at all. |
| Ref destRef = git.getRef(change.getDest().get()); |
| if (destRef == null) { |
| throw new IOException( |
| "The destination branch does not exist: " |
| + change.getDest().get()); |
| } |
| branchTipCommit = revWalk.parseCommit(destRef.getObjectId()); |
| } |
| |
| final RevCommit rebasedCommit; |
| final ObjectInserter oi = git.newObjectInserter(); |
| try { |
| ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get()); |
| ObjectId newId = oi.insert(rebaseCommit( |
| git, oi, revWalk.parseCommit(oldId), branchTipCommit, myIdent)); |
| oi.flush(); |
| rebasedCommit = revWalk.parseCommit(newId); |
| } finally { |
| oi.release(); |
| } |
| |
| change.nextPatchSetId(); |
| final PatchSet newPatchSet = new PatchSet(change.currPatchSetId()); |
| newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis())); |
| newPatchSet.setUploader(user.getAccountId()); |
| newPatchSet.setRevision(new RevId(rebasedCommit.name())); |
| newPatchSet.setDraft(originalPatchSet.isDraft()); |
| |
| final PatchSetInfo info = |
| patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId()); |
| |
| RefUpdate ru = git.updateRef(newPatchSet.getRefName()); |
| ru.setExpectedOldObjectId(ObjectId.zeroId()); |
| ru.setNewObjectId(rebasedCommit); |
| 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())); |
| } |
| replication.fire(change.getProject(), ru.getName()); |
| |
| final Set<Account.Id> oldReviewers = Sets.newHashSet(); |
| final Set<Account.Id> oldCC = Sets.newHashSet(); |
| db.changes().beginTransaction(change.getId()); |
| try { |
| Change updatedChange; |
| |
| updatedChange = db.changes().atomicUpdate(changeId, |
| new AtomicUpdate<Change>() { |
| @Override |
| public Change update(Change change) { |
| if (change.getStatus().isOpen()) { |
| change.updateNumberOfPatchSets(newPatchSet.getPatchSetId()); |
| return change; |
| } else { |
| return null; |
| } |
| } |
| }); |
| if (updatedChange != null) { |
| change = updatedChange; |
| } else { |
| throw new InvalidChangeOperationException( |
| String.format("Change %s is closed", change.getId())); |
| } |
| |
| insertAncestors(db, newPatchSet.getId(), rebasedCommit); |
| db.patchSets().insert(Collections.singleton(newPatchSet)); |
| updatedChange = db.changes().atomicUpdate(changeId, |
| new AtomicUpdate<Change>() { |
| @Override |
| public Change update(Change change) { |
| if (change.getStatus().isClosed()) { |
| return null; |
| } |
| if (!change.currentPatchSetId().equals(patchSetId)) { |
| return null; |
| } |
| if (change.getStatus() != Change.Status.DRAFT) { |
| change.setStatus(Change.Status.NEW); |
| } |
| change.setLastSha1MergeTested(null); |
| change.setCurrentPatchSet(info); |
| ChangeUtil.updated(change); |
| return change; |
| } |
| }); |
| if (updatedChange != null) { |
| change = updatedChange; |
| } else { |
| throw new InvalidChangeOperationException( |
| String.format("Change %s was modified", change.getId())); |
| } |
| |
| for (PatchSetApproval a : approvalsUtil.copyVetosToLatestPatchSet(change)) { |
| if (a.getValue() != 0) { |
| oldReviewers.add(a.getAccountId()); |
| } else { |
| oldCC.add(a.getAccountId()); |
| } |
| } |
| |
| final ChangeMessage cmsg = |
| new ChangeMessage(new ChangeMessage.Key(changeId, |
| ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId); |
| cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased"); |
| db.changeMessages().insert(Collections.singleton(cmsg)); |
| db.commit(); |
| } finally { |
| db.rollback(); |
| } |
| |
| final ReplacePatchSetSender cm = |
| rebasedPatchSetSenderFactory.create(change); |
| cm.setFrom(user.getAccountId()); |
| cm.setPatchSet(newPatchSet); |
| cm.addReviewers(oldReviewers); |
| cm.addExtraCC(oldCC); |
| cm.send(); |
| |
| hooks.doPatchsetCreatedHook(change, newPatchSet, db); |
| } finally { |
| revWalk.release(); |
| } |
| } finally { |
| git.close(); |
| } |
| } |
| |
| public static Change.Id revert(final PatchSet.Id patchSetId, |
| final IdentifiedUser user, final String message, final ReviewDb db, |
| final RevertedSender.Factory revertedSenderFactory, |
| final ChangeHooks hooks, GitRepositoryManager gitManager, |
| final PatchSetInfoFactory patchSetInfoFactory, |
| final GitReferenceUpdated replication, PersonIdent myIdent) |
| throws NoSuchChangeException, EmailException, OrmException, |
| MissingObjectException, IncorrectObjectTypeException, IOException { |
| final Change.Id changeId = patchSetId.getParentKey(); |
| final PatchSet patch = db.patchSets().get(patchSetId); |
| if (patch == null) { |
| throw new NoSuchChangeException(changeId); |
| } |
| |
| final Repository git; |
| try { |
| git = gitManager.openRepository(db.changes().get(changeId).getProject()); |
| } catch (RepositoryNotFoundException e) { |
| throw new NoSuchChangeException(changeId, e); |
| } |
| |
| final RevWalk revWalk = new RevWalk(git); |
| try { |
| RevCommit commitToRevert = |
| revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get())); |
| |
| PersonIdent authorIdent = |
| user.newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone()); |
| |
| RevCommit parentToCommitToRevert = commitToRevert.getParent(0); |
| revWalk.parseHeaders(parentToCommitToRevert); |
| |
| CommitBuilder revertCommitBuilder = new CommitBuilder(); |
| revertCommitBuilder.addParentId(commitToRevert); |
| revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree()); |
| revertCommitBuilder.setAuthor(authorIdent); |
| revertCommitBuilder.setCommitter(myIdent); |
| |
| final ObjectId computedChangeId = |
| ChangeIdUtil.computeChangeId(parentToCommitToRevert.getTree(), |
| commitToRevert, authorIdent, myIdent, message); |
| revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, computedChangeId, true)); |
| |
| RevCommit revertCommit; |
| final ObjectInserter oi = git.newObjectInserter(); |
| try { |
| ObjectId id = oi.insert(revertCommitBuilder); |
| oi.flush(); |
| revertCommit = revWalk.parseCommit(id); |
| } finally { |
| oi.release(); |
| } |
| |
| final Change change = new Change( |
| new Change.Key("I" + computedChangeId.name()), |
| new Change.Id(db.nextChangeId()), |
| user.getAccountId(), |
| db.changes().get(changeId).getDest()); |
| change.nextPatchSetId(); |
| |
| final PatchSet ps = new PatchSet(change.currPatchSetId()); |
| ps.setCreatedOn(change.getCreatedOn()); |
| ps.setUploader(change.getOwner()); |
| ps.setRevision(new RevId(revertCommit.name())); |
| |
| change.setCurrentPatchSet(patchSetInfoFactory.get(revertCommit, ps.getId())); |
| ChangeUtil.updated(change); |
| |
| final RefUpdate ru = git.updateRef(ps.getRefName()); |
| ru.setExpectedOldObjectId(ObjectId.zeroId()); |
| ru.setNewObjectId(revertCommit); |
| ru.disableRefLog(); |
| if (ru.update(revWalk) != RefUpdate.Result.NEW) { |
| throw new IOException(String.format( |
| "Failed to create ref %s in %s: %s", ps.getRefName(), |
| change.getDest().getParentKey().get(), ru.getResult())); |
| } |
| replication.fire(change.getProject(), ru.getName()); |
| |
| db.changes().beginTransaction(change.getId()); |
| try { |
| insertAncestors(db, ps.getId(), revertCommit); |
| db.patchSets().insert(Collections.singleton(ps)); |
| db.changes().insert(Collections.singleton(change)); |
| db.commit(); |
| } finally { |
| db.rollback(); |
| } |
| |
| final ChangeMessage cmsg = |
| new ChangeMessage(new ChangeMessage.Key(changeId, |
| ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId); |
| final StringBuilder msgBuf = |
| new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted"); |
| msgBuf.append("\n\n"); |
| msgBuf.append("This patchset was reverted in change: " + change.getKey().get()); |
| |
| cmsg.setMessage(msgBuf.toString()); |
| db.changeMessages().insert(Collections.singleton(cmsg)); |
| |
| final RevertedSender cm = revertedSenderFactory.create(change); |
| cm.setFrom(user.getAccountId()); |
| cm.setChangeMessage(cmsg); |
| cm.send(); |
| |
| hooks.doPatchsetCreatedHook(change, ps, db); |
| |
| return change.getId(); |
| } finally { |
| revWalk.release(); |
| git.close(); |
| } |
| } |
| |
| public static void deleteDraftChange(final PatchSet.Id patchSetId, |
| GitRepositoryManager gitManager, |
| final GitReferenceUpdated replication, final ReviewDb db) |
| throws NoSuchChangeException, OrmException, IOException { |
| final Change.Id changeId = patchSetId.getParentKey(); |
| final Change change = db.changes().get(changeId); |
| if (change == null || change.getStatus() != Change.Status.DRAFT) { |
| throw new NoSuchChangeException(changeId); |
| } |
| |
| for (PatchSet ps : db.patchSets().byChange(changeId)) { |
| // These should all be draft patch sets. |
| deleteOnlyDraftPatchSet(ps, change, gitManager, replication, db); |
| } |
| |
| db.changeMessages().delete(db.changeMessages().byChange(changeId)); |
| db.starredChanges().delete(db.starredChanges().byChange(changeId)); |
| db.trackingIds().delete(db.trackingIds().byChange(changeId)); |
| db.changes().delete(Collections.singleton(change)); |
| } |
| |
| public static void deleteOnlyDraftPatchSet(final PatchSet patch, |
| final Change change, GitRepositoryManager gitManager, |
| final GitReferenceUpdated replication, final ReviewDb db) |
| throws NoSuchChangeException, OrmException, IOException { |
| final PatchSet.Id patchSetId = patch.getId(); |
| if (patch == null || !patch.isDraft()) { |
| throw new NoSuchChangeException(patchSetId.getParentKey()); |
| } |
| |
| Repository repo = gitManager.openRepository(change.getProject()); |
| try { |
| RefUpdate update = repo.updateRef(patch.getRefName()); |
| update.setForceUpdate(true); |
| update.disableRefLog(); |
| switch (update.delete()) { |
| case NEW: |
| case FAST_FORWARD: |
| case FORCED: |
| case NO_CHANGE: |
| // Successful deletion. |
| break; |
| default: |
| throw new IOException("Failed to delete ref " + patch.getRefName() + |
| " in " + repo.getDirectory() + ": " + update.getResult()); |
| } |
| replication.fire(change.getProject(), update.getName()); |
| } finally { |
| repo.close(); |
| } |
| |
| db.accountPatchReviews().delete(db.accountPatchReviews().byPatchSet(patchSetId)); |
| db.changeMessages().delete(db.changeMessages().byPatchSet(patchSetId)); |
| db.patchComments().delete(db.patchComments().byPatchSet(patchSetId)); |
| db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(patchSetId)); |
| db.patchSetAncestors().delete(db.patchSetAncestors().byPatchSet(patchSetId)); |
| |
| db.patchSets().delete(Collections.singleton(patch)); |
| } |
| |
| public static <T extends ReplyToChangeSender> void updatedChange( |
| final ReviewDb db, final IdentifiedUser user, final Change change, |
| final ChangeMessage cmsg, ReplyToChangeSender.Factory<T> senderFactory) |
| throws OrmException { |
| db.changeMessages().insert(Collections.singleton(cmsg)); |
| |
| new ApprovalsUtil(db, null).syncChangeStatus(change); |
| |
| // Email the reviewers |
| try { |
| final ReplyToChangeSender cm = senderFactory.create(change); |
| cm.setFrom(user.getAccountId()); |
| cm.setChangeMessage(cmsg); |
| cm.send(); |
| } catch (Exception e) { |
| log.error("Cannot email update for change " + change.getChangeId(), e); |
| } |
| } |
| |
| public static String sortKey(long lastUpdated, int id){ |
| // The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC. |
| // We overrun approximately 4,085 years later, so ~6093. |
| // |
| final long lastUpdatedOn = (lastUpdated / 1000L) - 1222819200L; |
| final StringBuilder r = new StringBuilder(16); |
| r.setLength(16); |
| formatHexInt(r, 0, (int) (lastUpdatedOn / 60)); |
| formatHexInt(r, 8, id); |
| return r.toString(); |
| } |
| |
| public static void computeSortKey(final Change c) { |
| long lastUpdated = c.getLastUpdatedOn().getTime(); |
| int id = c.getId().get(); |
| c.setSortKey(sortKey(lastUpdated, id)); |
| } |
| |
| private static final char[] hexchar = |
| {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', // |
| 'a', 'b', 'c', 'd', 'e', 'f'}; |
| |
| private static void formatHexInt(final StringBuilder dst, final int p, int w) { |
| int o = p + 7; |
| while (o >= p && w != 0) { |
| dst.setCharAt(o--, hexchar[w & 0xf]); |
| w >>>= 4; |
| } |
| while (o >= p) { |
| dst.setCharAt(o--, '0'); |
| } |
| } |
| } |