| // 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 static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT; |
| |
| import com.google.gerrit.reviewdb.Change; |
| import com.google.gerrit.reviewdb.PatchSet; |
| import com.google.gerrit.reviewdb.PatchSetApproval; |
| import com.google.gerrit.reviewdb.ReviewDb; |
| import com.google.gerrit.reviewdb.TrackingId; |
| import com.google.gerrit.server.config.TrackingFooter; |
| import com.google.gerrit.server.config.TrackingFooters; |
| import com.google.gerrit.server.git.MergeOp; |
| import com.google.gerrit.server.git.MergeQueue; |
| import com.google.gwtorm.client.AtomicUpdate; |
| import com.google.gwtorm.client.OrmConcurrencyException; |
| import com.google.gwtorm.client.OrmException; |
| |
| import org.eclipse.jgit.revwalk.FooterLine; |
| import org.eclipse.jgit.util.Base64; |
| import org.eclipse.jgit.util.NB; |
| |
| import java.util.ArrayList; |
| 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 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 submit(MergeOp.Factory opFactory, PatchSet.Id patchSetId, |
| IdentifiedUser user, ReviewDb db, MergeQueue merger) throws OrmException { |
| final Change.Id changeId = patchSetId.getParentKey(); |
| final PatchSetApproval approval = createSubmitApproval(patchSetId, user, db); |
| |
| db.patchSetApprovals().upsert(Collections.singleton(approval)); |
| |
| final Change change = db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() { |
| @Override |
| public Change update(Change change) { |
| if (change.getStatus() == Change.Status.NEW) { |
| change.setStatus(Change.Status.SUBMITTED); |
| ChangeUtil.updated(change); |
| } |
| return change; |
| } |
| }); |
| |
| if (change.getStatus() == Change.Status.SUBMITTED) { |
| merger.merge(opFactory, change.getDest()); |
| } |
| } |
| |
| public static PatchSetApproval createSubmitApproval(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db) |
| throws OrmException { |
| final List<PatchSetApproval> allApprovals = |
| new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet( |
| patchSetId).toList()); |
| |
| final PatchSetApproval.Key akey = |
| new PatchSetApproval.Key(patchSetId, user.getAccountId(), SUBMIT); |
| |
| for (final PatchSetApproval approval : allApprovals) { |
| if (akey.equals(approval.getKey())) { |
| approval.setValue((short) 1); |
| approval.setGranted(); |
| return approval; |
| } |
| } |
| return new PatchSetApproval(akey, (short) 1); |
| } |
| |
| 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'); |
| } |
| } |
| } |