| // Copyright (C) 2014 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.notedb; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkState; |
| |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.Change; |
| import com.google.gerrit.reviewdb.client.Comment; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.InternalUser; |
| import com.google.gerrit.server.project.ChangeControl; |
| import com.google.gwtorm.server.OrmException; |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| import java.util.Date; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| /** A single delta related to a specific patch-set of a change. */ |
| public abstract class AbstractChangeUpdate { |
| protected final NotesMigration migration; |
| protected final ChangeNoteUtil noteUtil; |
| protected final String anonymousCowardName; |
| protected final Account.Id accountId; |
| protected final Account.Id realAccountId; |
| protected final PersonIdent authorIdent; |
| protected final Date when; |
| private final long readOnlySkewMs; |
| |
| @Nullable private final ChangeNotes notes; |
| private final Change change; |
| protected final PersonIdent serverIdent; |
| |
| protected PatchSet.Id psId; |
| private ObjectId result; |
| |
| protected AbstractChangeUpdate( |
| Config cfg, |
| NotesMigration migration, |
| ChangeControl ctl, |
| PersonIdent serverIdent, |
| String anonymousCowardName, |
| ChangeNoteUtil noteUtil, |
| Date when) { |
| this.migration = migration; |
| this.noteUtil = noteUtil; |
| this.serverIdent = new PersonIdent(serverIdent, when); |
| this.anonymousCowardName = anonymousCowardName; |
| this.notes = ctl.getNotes(); |
| this.change = notes.getChange(); |
| this.accountId = accountId(ctl.getUser()); |
| Account.Id realAccountId = accountId(ctl.getUser().getRealUser()); |
| this.realAccountId = realAccountId != null ? realAccountId : accountId; |
| this.authorIdent = ident(noteUtil, serverIdent, anonymousCowardName, ctl.getUser(), when); |
| this.when = when; |
| this.readOnlySkewMs = NoteDbChangeState.getReadOnlySkew(cfg); |
| } |
| |
| protected AbstractChangeUpdate( |
| Config cfg, |
| NotesMigration migration, |
| ChangeNoteUtil noteUtil, |
| PersonIdent serverIdent, |
| String anonymousCowardName, |
| @Nullable ChangeNotes notes, |
| @Nullable Change change, |
| Account.Id accountId, |
| Account.Id realAccountId, |
| PersonIdent authorIdent, |
| Date when) { |
| checkArgument( |
| (notes != null && change == null) || (notes == null && change != null), |
| "exactly one of notes or change required"); |
| this.migration = migration; |
| this.noteUtil = noteUtil; |
| this.serverIdent = new PersonIdent(serverIdent, when); |
| this.anonymousCowardName = anonymousCowardName; |
| this.notes = notes; |
| this.change = change != null ? change : notes.getChange(); |
| this.accountId = accountId; |
| this.realAccountId = realAccountId; |
| this.authorIdent = authorIdent; |
| this.when = when; |
| this.readOnlySkewMs = NoteDbChangeState.getReadOnlySkew(cfg); |
| } |
| |
| private static void checkUserType(CurrentUser user) { |
| checkArgument( |
| (user instanceof IdentifiedUser) || (user instanceof InternalUser), |
| "user must be IdentifiedUser or InternalUser: %s", |
| user); |
| } |
| |
| private static Account.Id accountId(CurrentUser u) { |
| checkUserType(u); |
| return (u instanceof IdentifiedUser) ? u.getAccountId() : null; |
| } |
| |
| private static PersonIdent ident( |
| ChangeNoteUtil noteUtil, |
| PersonIdent serverIdent, |
| String anonymousCowardName, |
| CurrentUser u, |
| Date when) { |
| checkUserType(u); |
| if (u instanceof IdentifiedUser) { |
| return noteUtil.newIdent( |
| u.asIdentifiedUser().getAccount(), when, serverIdent, anonymousCowardName); |
| } else if (u instanceof InternalUser) { |
| return serverIdent; |
| } |
| throw new IllegalStateException(); |
| } |
| |
| public Change.Id getId() { |
| return change.getId(); |
| } |
| |
| /** |
| * @return notes for the state of this change prior to this update. If this update is part of a |
| * series managed by a {@link NoteDbUpdateManager}, then this reflects the state prior to the |
| * first update in the series. A null return value can only happen when the change is being |
| * rebuilt from NoteDb. A change that is in the process of being created will result in a |
| * non-null return value from this method, but a null return value from {@link |
| * ChangeNotes#getRevision()}. |
| */ |
| @Nullable |
| public ChangeNotes getNotes() { |
| return notes; |
| } |
| |
| public Change getChange() { |
| return change; |
| } |
| |
| public Date getWhen() { |
| return when; |
| } |
| |
| public PatchSet.Id getPatchSetId() { |
| return psId; |
| } |
| |
| public void setPatchSetId(PatchSet.Id psId) { |
| checkArgument(psId == null || psId.getParentKey().equals(getId())); |
| this.psId = psId; |
| } |
| |
| public Account.Id getAccountId() { |
| checkState( |
| accountId != null, |
| "author identity for %s is not from an IdentifiedUser: %s", |
| getClass().getSimpleName(), |
| authorIdent.toExternalString()); |
| return accountId; |
| } |
| |
| public Account.Id getNullableAccountId() { |
| return accountId; |
| } |
| |
| protected PersonIdent newIdent(Account author, Date when) { |
| return noteUtil.newIdent(author, when, serverIdent, anonymousCowardName); |
| } |
| |
| /** Whether no updates have been done. */ |
| public abstract boolean isEmpty(); |
| |
| /** |
| * @return the NameKey for the project where the update will be stored, which is not necessarily |
| * the same as the change's project. |
| */ |
| protected abstract Project.NameKey getProjectName(); |
| |
| protected abstract String getRefName(); |
| |
| /** |
| * Apply this update to the given inserter. |
| * |
| * @param rw walk for reading back any objects needed for the update. |
| * @param ins inserter to write to; callers should not flush. |
| * @param curr the current tip of the branch prior to this update. |
| * @return commit ID produced by inserting this update's commit, or null if this update is a no-op |
| * and should be skipped. The zero ID is a valid return value, and indicates the ref should be |
| * deleted. |
| * @throws OrmException if a Gerrit-level error occurred. |
| * @throws IOException if a lower-level error occurred. |
| */ |
| final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr) |
| throws OrmException, IOException { |
| if (isEmpty()) { |
| return null; |
| } |
| |
| // Allow this method to proceed even if migration.failChangeWrites() = true. |
| // This may be used by an auto-rebuilding step that the caller does not plan |
| // to actually store. |
| |
| checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins); |
| checkNotReadOnly(); |
| ObjectId z = ObjectId.zeroId(); |
| CommitBuilder cb = applyImpl(rw, ins, curr); |
| if (cb == null) { |
| result = z; |
| return z; // Impl intends to delete the ref. |
| } else if (cb == NO_OP_UPDATE) { |
| return null; // Impl is a no-op. |
| } |
| cb.setAuthor(authorIdent); |
| cb.setCommitter(new PersonIdent(serverIdent, when)); |
| if (!curr.equals(z)) { |
| cb.setParentId(curr); |
| } else { |
| cb.setParentIds(); // Ref is currently nonexistent, commit has no parents. |
| } |
| if (cb.getTreeId() == null) { |
| if (curr.equals(z)) { |
| cb.setTreeId(emptyTree(ins)); // No parent, assume empty tree. |
| } else { |
| RevCommit p = rw.parseCommit(curr); |
| cb.setTreeId(p.getTree()); // Copy tree from parent. |
| } |
| } |
| result = ins.insert(cb); |
| return result; |
| } |
| |
| protected void checkNotReadOnly() throws OrmException { |
| ChangeNotes notes = getNotes(); |
| if (notes == null) { |
| // Can only happen during ChangeRebuilder, which will never include a read-only lease. |
| return; |
| } |
| Timestamp until = notes.getReadOnlyUntil(); |
| if (until != null && NoteDbChangeState.timeForReadOnlyCheck(readOnlySkewMs).before(until)) { |
| throw new OrmException("change " + notes.getChangeId() + " is read-only until " + until); |
| } |
| } |
| |
| /** |
| * Create a commit containing the contents of this update. |
| * |
| * @param ins inserter to write to; callers should not flush. |
| * @return a new commit builder representing this commit, or null to indicate the meta ref should |
| * be deleted as a result of this update. The parent, author, and committer fields in the |
| * return value are always overwritten. The tree ID may be unset by this method, which |
| * indicates to the caller that it should be copied from the parent commit. To indicate that |
| * this update is a no-op (but this could not be determined by {@link #isEmpty()}), return the |
| * sentinel {@link #NO_OP_UPDATE}. |
| * @throws OrmException if a Gerrit-level error occurred. |
| * @throws IOException if a lower-level error occurred. |
| */ |
| protected abstract CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr) |
| throws OrmException, IOException; |
| |
| protected static final CommitBuilder NO_OP_UPDATE = new CommitBuilder(); |
| |
| ObjectId getResult() { |
| return result; |
| } |
| |
| public boolean allowWriteToNewRef() { |
| return true; |
| } |
| |
| private static ObjectId emptyTree(ObjectInserter ins) throws IOException { |
| return ins.insert(Constants.OBJ_TREE, new byte[] {}); |
| } |
| |
| protected void verifyComment(Comment c) { |
| checkArgument(c.revId != null, "RevId required for comment: %s", c); |
| checkArgument( |
| c.author.getId().equals(getAccountId()), |
| "The author for the following comment does not match the author of this %s (%s): %s", |
| getClass().getSimpleName(), |
| getAccountId(), |
| c); |
| checkArgument( |
| c.getRealAuthor().getId().equals(realAccountId), |
| "The real author for the following comment does not match the real" |
| + " author of this %s (%s): %s", |
| getClass().getSimpleName(), |
| realAccountId, |
| c); |
| } |
| } |