blob: 6886d2c85d93c8d9f1d063d31cd2616b78cfd135 [file] [log] [blame]
// 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.common.ChangeHookRunner;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.ChangeMessage;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.PatchSetInfo;
import com.google.gerrit.reviewdb.RevId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.TrackingId;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.TrackingFooter;
import com.google.gerrit.server.config.TrackingFooters;
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.ReplicationQueue;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RevertedSender;
import com.google.gwtorm.client.AtomicUpdate;
import com.google.gwtorm.client.OrmConcurrencyException;
import com.google.gwtorm.client.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.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.util.Base64;
import org.eclipse.jgit.util.NB;
import java.io.IOException;
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(final PatchSet.Id patchSetId,
final IdentifiedUser user, final ReviewDb db,
final MergeOp.Factory opFactory, final 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 updatedChange = 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 (updatedChange.getStatus() == Change.Status.SUBMITTED) {
merger.merge(opFactory, updatedChange.getDest());
}
}
public static PatchSetApproval createSubmitApproval(
final PatchSet.Id patchSetId, final IdentifiedUser user, final 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 void abandon(final PatchSet.Id patchSetId,
final IdentifiedUser user, final String message, final ReviewDb db,
final AbandonedSender.Factory abandonedSenderFactory,
final ChangeHookRunner hooks) throws NoSuchChangeException,
EmailException, OrmException {
final Change.Id changeId = patchSetId.getParentKey();
final PatchSet patch = db.patchSets().get(patchSetId);
if (patch == null) {
throw new NoSuchChangeException(changeId);
}
final ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
.messageUUID(db)), user.getAccountId());
final StringBuilder msgBuf =
new StringBuilder("Patch Set " + patchSetId.get() + ": Abandoned");
if (message != null && message.length() > 0) {
msgBuf.append("\n\n");
msgBuf.append(message);
}
cmsg.setMessage(msgBuf.toString());
final Change updatedChange = db.changes().atomicUpdate(changeId,
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus().isOpen()
&& change.currentPatchSetId().equals(patchSetId)) {
change.setStatus(Change.Status.ABANDONED);
ChangeUtil.updated(change);
return change;
} else {
return null;
}
}
});
if (updatedChange != null) {
db.changeMessages().insert(Collections.singleton(cmsg));
final List<PatchSetApproval> approvals =
db.patchSetApprovals().byChange(changeId).toList();
for (PatchSetApproval a : approvals) {
a.cache(updatedChange);
}
db.patchSetApprovals().update(approvals);
// Email the reviewers
final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
cm.setFrom(user.getAccountId());
cm.setChangeMessage(cmsg);
cm.send();
}
hooks.doChangeAbandonedHook(updatedChange, user.getAccount(), message);
}
public static void revert(final PatchSet.Id patchSetId,
final IdentifiedUser user, final String message, final ReviewDb db,
final RevertedSender.Factory revertedSenderFactory,
final ChangeHookRunner hooks, GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
final ReplicationQueue replication, PersonIdent myIdent)
throws NoSuchChangeException, EmailException, OrmException,
MissingObjectException, IncorrectObjectTypeException, IOException,
PatchSetInfoNotAvailableException {
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 revertCommit = new CommitBuilder();
revertCommit.addParentId(commitToRevert);
revertCommit.setTreeId(parentToCommitToRevert.getTree());
revertCommit.setAuthor(authorIdent);
revertCommit.setCommitter(myIdent);
revertCommit.setMessage(message);
final ObjectInserter oi = git.newObjectInserter();;
ObjectId id;
try {
id = oi.insert(revertCommit);
oi.flush();
} finally {
oi.release();
}
Change.Key changeKey = new Change.Key("I" + id.name());
final Change change =
new Change(changeKey, 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(user.getAccountId());
ps.setRevision(new RevId(id.getName()));
db.patchSets().insert(Collections.singleton(ps));
final PatchSetInfo info =
patchSetInfoFactory.get(revWalk.parseCommit(id), ps.getId());
change.setCurrentPatchSet(info);
ChangeUtil.updated(change);
db.changes().insert(Collections.singleton(change));
final RefUpdate ru = git.updateRef(ps.getRefName());
ru.setNewObjectId(id);
ru.disableRefLog();
if (ru.update(revWalk) != RefUpdate.Result.NEW) {
throw new IOException("Failed to create ref " + ps.getRefName()
+ " in " + git.getDirectory() + ": " + ru.getResult());
}
replication.scheduleUpdate(db.changes().get(changeId).getProject(),
ru.getName());
final ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(changeId,
ChangeUtil.messageUUID(db)), user.getAccountId());
final StringBuilder msgBuf =
new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
msgBuf.append("\n\n");
msgBuf.append("This patchset was reverted in change: " + changeKey.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);
} finally {
revWalk.release();
git.close();
}
}
public static void restore(final PatchSet.Id patchSetId,
final IdentifiedUser user, final String message, final ReviewDb db,
final AbandonedSender.Factory abandonedSenderFactory,
final ChangeHookRunner hooks) throws NoSuchChangeException,
EmailException, OrmException {
final Change.Id changeId = patchSetId.getParentKey();
final PatchSet patch = db.patchSets().get(patchSetId);
if (patch == null) {
throw new NoSuchChangeException(changeId);
}
final ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
.messageUUID(db)), user.getAccountId());
final StringBuilder msgBuf =
new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
if (message != null && message.length() > 0) {
msgBuf.append("\n\n");
msgBuf.append(message);
}
cmsg.setMessage(msgBuf.toString());
final Change updatedChange = db.changes().atomicUpdate(changeId,
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
if (change.getStatus() == Change.Status.ABANDONED
&& change.currentPatchSetId().equals(patchSetId)) {
change.setStatus(Change.Status.NEW);
ChangeUtil.updated(change);
return change;
} else {
return null;
}
}
});
if (updatedChange != null) {
db.changeMessages().insert(Collections.singleton(cmsg));
final List<PatchSetApproval> approvals =
db.patchSetApprovals().byChange(changeId).toList();
for (PatchSetApproval a : approvals) {
a.cache(updatedChange);
}
db.patchSetApprovals().update(approvals);
// Email the reviewers
final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
cm.setFrom(user.getAccountId());
cm.setChangeMessage(cmsg);
cm.send();
}
hooks.doChangeRestoreHook(updatedChange, user.getAccount(), message);
}
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');
}
}
}