// Copyright (C) 2009 The Android Open Source Project // Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). // // 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.gerrit.common.ChangeHooks; import com.google.gerrit.common.data.LabelTypes; 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.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.PatchSetApproval.LabelId; 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.change.ChangeInserter; import com.google.gerrit.server.change.ChangeMessages; import com.google.gerrit.server.config.TrackingFooter; import com.google.gerrit.server.config.TrackingFooters; import com.google.gerrit.server.events.CommitReceivedEvent; 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.git.MergeQueue; import com.google.gerrit.server.git.StagingUtil; import com.google.gerrit.server.git.validators.CommitValidationException; import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.mail.CommitMessageEditedSender; import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals; import com.google.gerrit.server.mail.MailUtil.MailRecipients; import com.google.gerrit.server.mail.RevertedSender; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; 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.NoSuchRefException; import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.util.IdGenerator; import com.google.gerrit.server.util.MagicBranch; 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.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.revwalk.FooterLine; 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 java.io.IOException; import java.sql.Timestamp; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; public class ChangeUtil { private static final Object uuidLock = new Object(); private static final int SEED = 0x2418e6f9; 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(ReviewDb db) throws OrmException { int p, s; synchronized (uuidLock) { if (uuidSeq == 0) { uuidPrefix = db.nextChangeMessageId(); uuidSeq = Integer.MAX_VALUE; } p = uuidPrefix; s = uuidSeq--; } String u = IdGenerator.format(IdGenerator.mix(SEED, p)); String l = IdGenerator.format(IdGenerator.mix(p, s)); return u + '_' + l; } 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 footerLines) throws OrmException { if (trackingFooters.getTrackingFooters().isEmpty() || footerLines.isEmpty()) { return; } final Set want = new HashSet(); final Set have = new HashSet( // 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 toInsert = new HashSet(want); final Set toDelete = new HashSet(have); toInsert.removeAll(have); toDelete.removeAll(want); db.trackingIds().insert(toInsert); db.trackingIds().delete(toDelete); } public static void testMerge(MergeOp.Factory opFactory, Change change) throws NoSuchProjectException { 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 toInsert = new ArrayList(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); } public static Change.Id revert(RefControl refControl, PatchSet.Id patchSetId, IdentifiedUser user, CommitValidators commitValidators, String message, ReviewDb db, RevertedSender.Factory revertedSenderFactory, ChangeHooks hooks, Repository git, PatchSetInfoFactory patchSetInfoFactory, PersonIdent myIdent, ChangeInserter changeInserter) throws NoSuchChangeException, EmailException, OrmException, MissingObjectException, IncorrectObjectTypeException, IOException, InvalidChangeOperationException { final Change.Id changeId = patchSetId.getParentKey(); final PatchSet patch = db.patchSets().get(patchSetId); if (patch == null) { throw new NoSuchChangeException(changeId); } final Change changeToRevert = db.changes().get(changeId); 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); if (message == null) { message = MessageFormat.format( ChangeMessages.get().revertChangeDefaultMessage, changeToRevert.getSubject(), patch.getRevision().get()); } 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(), changeToRevert.getDest()); change.setTopic(changeToRevert.getTopic()); PatchSet.Id id = new PatchSet.Id(change.getId(), Change.INITIAL_PATCH_SET_ID); final PatchSet ps = new PatchSet(id); ps.setCreatedOn(change.getCreatedOn()); ps.setUploader(change.getOwner()); ps.setRevision(new RevId(revertCommit.name())); String ref = refControl.getRefName(); final String cmdRef = MagicBranch.NEW_PUBLISH_CHANGE + ref.substring(ref.lastIndexOf("/") + 1); CommitReceivedEvent commitReceivedEvent = new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(), revertCommit.getId(), cmdRef), refControl.getProjectControl() .getProject(), refControl.getRefName(), revertCommit, user); try { commitValidators.validateForGerritCommits(commitReceivedEvent); } catch (CommitValidationException e) { throw new InvalidChangeOperationException(e.getMessage()); } PatchSetInfo info = patchSetInfoFactory.get(revertCommit, ps.getId()); change.setCurrentPatchSet(info); 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())); } 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()); LabelTypes labelTypes = refControl.getProjectControl().getLabelTypes(); changeInserter.insertChange(db, change, cmsg, ps, revertCommit, labelTypes, info, Collections. emptySet()); final RevertedSender cm = revertedSenderFactory.create(change); cm.setFrom(user.getAccountId()); cm.setChangeMessage(cmsg); cm.send(); return change.getId(); } finally { revWalk.release(); } } public static Change.Id editCommitMessage(final PatchSet.Id patchSetId, final RefControl refControl, CommitValidators commitValidators, final IdentifiedUser user, final String message, final ReviewDb db, final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory, final ChangeHooks hooks, Repository git, final PatchSetInfoFactory patchSetInfoFactory, final GitReferenceUpdated gitRefUpdated, PersonIdent myIdent, final TrackingFooters trackingFooters) throws NoSuchChangeException, EmailException, OrmException, MissingObjectException, IncorrectObjectTypeException, IOException, InvalidChangeOperationException, PatchSetInfoNotAvailableException { final Change.Id changeId = patchSetId.getParentKey(); final PatchSet originalPS = db.patchSets().get(patchSetId); if (originalPS == null) { throw new NoSuchChangeException(changeId); } if (message == null || message.length() == 0) { throw new InvalidChangeOperationException("The commit message cannot be empty"); } final RevWalk revWalk = new RevWalk(git); try { RevCommit commit = revWalk.parseCommit(ObjectId.fromString(originalPS.getRevision().get())); if (commit.getFullMessage().equals(message)) { throw new InvalidChangeOperationException("New commit message cannot be same as existing commit message"); } Date now = myIdent.getWhen(); Change change = db.changes().get(changeId); PersonIdent authorIdent = user.newCommitterIdent(now, myIdent.getTimeZone()); CommitBuilder commitBuilder = new CommitBuilder(); commitBuilder.setTreeId(commit.getTree()); commitBuilder.setParentIds(commit.getParents()); commitBuilder.setAuthor(commit.getAuthorIdent()); commitBuilder.setCommitter(authorIdent); commitBuilder.setMessage(message); RevCommit newCommit; final ObjectInserter oi = git.newObjectInserter(); try { ObjectId id = oi.insert(commitBuilder); oi.flush(); newCommit = revWalk.parseCommit(id); } finally { oi.release(); } PatchSet.Id id = nextPatchSetId(git, change.currentPatchSetId()); final PatchSet newPatchSet = new PatchSet(id); newPatchSet.setCreatedOn(new Timestamp(now.getTime())); newPatchSet.setUploader(user.getAccountId()); newPatchSet.setRevision(new RevId(newCommit.name())); newPatchSet.setDraft(originalPS.isDraft()); final PatchSetInfo info = patchSetInfoFactory.get(newCommit, newPatchSet.getId()); final String refName = newPatchSet.getRefName(); CommitReceivedEvent commitReceivedEvent = new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(), newCommit.getId(), refName.substring(0, refName.lastIndexOf("/") + 1) + "new"), refControl .getProjectControl().getProject(), refControl.getRefName(), newCommit, user); try { commitValidators.validateForReceiveCommits(commitReceivedEvent); } catch (CommitValidationException e) { throw new InvalidChangeOperationException(e.getMessage()); } final RefUpdate ru = git.updateRef(newPatchSet.getRefName()); ru.setExpectedOldObjectId(ObjectId.zeroId()); ru.setNewObjectId(newCommit); 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())); } gitRefUpdated.fire(change.getProject(), ru); db.changes().beginTransaction(change.getId()); try { Change updatedChange = db.changes().get(change.getId()); if (updatedChange != null && updatedChange.getStatus().isOpen()) { change = updatedChange; } else { throw new InvalidChangeOperationException(String.format( "Change %s is closed", change.getId())); } ChangeUtil.insertAncestors(db, newPatchSet.getId(), commit); db.patchSets().insert(Collections.singleton(newPatchSet)); updatedChange = db.changes().atomicUpdate(change.getId(), new AtomicUpdate() { @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())); } ApprovalsUtil.copyLabels(db, refControl.getProjectControl().getLabelTypes(), originalPS.getId(), change.currentPatchSetId()); final List footerLines = newCommit.getFooterLines(); updateTrackingIds(db, change, trackingFooters, footerLines); final ChangeMessage cmsg = new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId); final String msg = "Patch Set " + newPatchSet.getPatchSetId() + ": Commit message was updated"; cmsg.setMessage(msg); db.changeMessages().insert(Collections.singleton(cmsg)); db.commit(); final CommitMessageEditedSender cm = commitMessageEditedSenderFactory.create(change); List oldChangeApprovals = db.patchSetApprovals().byChange(change.getId()).toList(); final MailRecipients oldRecipients = getRecipientsFromApprovals(oldChangeApprovals); cm.addReviewers(oldRecipients.getReviewers()); cm.addExtraCC(oldRecipients.getCcOnly()); cm.setFrom(user.getAccountId()); cm.setChangeMessage(cmsg); cm.send(); } finally { db.rollback(); } hooks.doPatchsetCreatedHook(change, newPatchSet, db); return change.getId(); } finally { revWalk.release(); } } public static void deleteDraftChange(final PatchSet.Id patchSetId, GitRepositoryManager gitManager, final GitReferenceUpdated gitRefUpdated, 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, gitRefUpdated, 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 gitRefUpdated, 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()); } gitRefUpdated.fire(change.getProject(), update); } 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 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)); } /** * Removes a commit from staging branch. Status of the change is reset to * NEW. * * @param patchSetId Patch set to be removed from staging. * @param user User taking this action. * @param db Review database. * @throws OrmException If review database cannot be accessed. */ public static Change rejectStagedChange(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db) throws OrmException { // Delete all STAGING approvals for the patch set. final PatchSetApproval.Key stagingKey = new PatchSetApproval.Key(patchSetId, user.getAccountId(), LabelId.STAGE); db.patchSetApprovals().deleteKeys(Collections.singleton(stagingKey)); // Set change state to NEW. final Change.Id changeId = patchSetId.getParentKey(); AtomicUpdate atomicUpdate = new AtomicUpdate() { @Override public Change update(Change change) { if (change.getStatus() == Change.Status.INTEGRATING || change.getStatus() == Change.Status.STAGING || change.getStatus() == Change.Status.STAGED) { change.setStatus(Change.Status.NEW); ChangeUtil.updated(change); } return change; } }; return db.changes().atomicUpdate(changeId, atomicUpdate); } /** * Moves change from integrating to merged. Only database is updated. * * @param patchSetId Patch set id for accessing the change. * @param user User taking the action. * @param db Review database. * @throws OrmException Thrown, if access to review database fails. */ public static void setIntegratingToMerged(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db) throws OrmException { final Change.Id changeId = patchSetId.getParentKey(); AtomicUpdate atomicUpdate = getUpdateToState(Change.Status.INTEGRATING, Change.Status.MERGED); Change change = db.changes().atomicUpdate(changeId, atomicUpdate); new ApprovalsUtil(db).syncChangeStatus(change); } /** * Moves change from integrating to abandoned. Only database is updated. * * @param patchSetId Patch set id for accessing the change. * @param user User taking the action. * @param db Review database. * @throws OrmException Thrown, if access to review database fails. */ public static void setIntegratingToAbandoned(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db) throws OrmException { final Change.Id changeId = patchSetId.getParentKey(); AtomicUpdate atomicUpdate = getUpdateToState(Change.Status.INTEGRATING, Change.Status.ABANDONED); Change change = db.changes().atomicUpdate(changeId, atomicUpdate); new ApprovalsUtil(db).syncChangeStatus(change); } /** * Reset the staging branch. This method should be called if some change * is removed from staging branch. For example, this method is called after * abandoning a change. * * @param branch Destination branch. E.g. refs/heads/master * @param user User taking this action. * @param db Review database. * @param git Git repository. * @param mergeFactory Merge operator factory. * @param merger Merge queue. * @param ChangeHookRunner Hooks runner. Ref update will be send as part * the rebuild. * @throws OrmException Thrown, if review database cannot be accessed. * @throws IOException Thrown, if Git repository cannot be accessed. * @throws NoSuchRefException Thrown, if destination branch is not available. */ public static void rebuildStaging(Branch.NameKey branch, IdentifiedUser user, ReviewDb db, Repository git, MergeQueue merger, ChangeHooks hooks) throws OrmException, IOException, NoSuchRefException { final Branch.NameKey stagingBranch = StagingUtil.getStagingBranch(branch); // Start staging branch from scratch. Ref ref = git.getRef(stagingBranch.get()); ObjectId oldTip = null; if (ref != null) { oldTip = ref.getObjectId(); } StagingUtil.createStagingBranch(git, branch); ObjectId newTip = git.getRef(branch.get()).getObjectId(); // If tips match, there was nothing in staging branch so no need to rebuild if (newTip.equals(oldTip)) { return; } hooks.doRefUpdatedHook(stagingBranch, oldTip, newTip, user.getAccount()); // Loop through all changes with status STAGED. List staged = db.changes().staged(branch).toList(); for (Change change : staged) { final PatchSet.Id currentPatchSet = change.currentPatchSetId(); final Change.Id changeId = currentPatchSet.getParentKey(); // Reset status to STAGING. AtomicUpdate atomicUpdate = getUpdateToState(Change.Status.STAGED, Change.Status.STAGING); db.changes().atomicUpdate(changeId, atomicUpdate); } // Merge all changes. merger.merge(stagingBranch); } public static void setIntegrating(PatchSet.Id patchSetId, ReviewDb db) throws OrmException { final Change.Id changeId = patchSetId.getParentKey(); AtomicUpdate atomicUpdate = getUpdateToState(Change.Status.STAGED, Change.Status.INTEGRATING); db.changes().atomicUpdate(changeId, atomicUpdate); } private static AtomicUpdate getUpdateToState(final Change.Status from, final Change.Status to) { return new AtomicUpdate() { @Override public Change update(Change change) { if (change.getStatus() == from) { change.setStatus(to); ChangeUtil.updated(change); } return change; } }; } public static PatchSet.Id nextPatchSetId(Map allRefs, PatchSet.Id id) { PatchSet.Id next = nextPatchSetId(id); while (allRefs.containsKey(next.toRefName())) { next = nextPatchSetId(next); } return next; } public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id) { return nextPatchSetId(git.getAllRefs(), id); } private static PatchSet.Id nextPatchSetId(PatchSet.Id id) { return new PatchSet.Id(id.getParentKey(), id.get() + 1); } 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'); } } }