// Copyright (C) 2008 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.reviewdb.client; import com.google.gwtorm.client.Column; import com.google.gwtorm.client.IntKey; import com.google.gwtorm.client.RowVersion; import com.google.gwtorm.client.StringKey; import java.sql.Timestamp; /** * A change proposed to be merged into a {@link Branch}. * <p> * The data graph rooted below a Change can be quite complex: * * <pre> * {@link Change} * | * +- {@link ChangeMessage}: "cover letter" or general comment. * | * +- {@link PatchSet}: a single variant of this change. * | * +- {@link PatchSetApproval}: a +/- vote on the change's current state. * | * +- {@link PatchSetAncestor}: parents of this change's commit. * | * +- {@link PatchLineComment}: comment about a specific line * </pre> * <p> * <h5>PatchSets</h5> * <p> * Every change has at least one PatchSet. A change starts out with one * PatchSet, the initial proposal put forth by the change owner. This * {@link Account} is usually also listed as the author and committer in the * PatchSetInfo. * <p> * The {@link PatchSetAncestor} entities are a mirror of the Git commit * metadata, providing access to the information without needing direct * accessing Git. These entities are actually legacy artifacts from Gerrit 1.x * and could be removed, replaced by direct RevCommit access. * <p> * Each PatchSet contains zero or more Patch records, detailing the file paths * impacted by the change (otherwise known as, the file paths the author * added/deleted/modified). Sometimes a merge commit can contain zero patches, * if the merge has no conflicts, or has no impact other than to cut off a line * of development. * <p> * Each PatchLineComment is a draft or a published comment about a single line * of the associated file. These are the inline comment entities created by * users as they perform a review. * <p> * When additional PatchSets appear under a change, these PatchSets reference * <i>replacement</i> commits; alternative commits that could be made to the * project instead of the original commit referenced by the first PatchSet. * <p> * A change has at most one current PatchSet. The current PatchSet is updated * when a new replacement PatchSet is uploaded. When a change is submitted, the * current patch set is what is merged into the destination branch. * <p> * <h5>ChangeMessage</h5> * <p> * The ChangeMessage entity is a general free-form comment about the whole * change, rather than PatchLineComment's file and line specific context. The * ChangeMessage appears at the start of any email generated by Gerrit, and is * shown on the change overview page, rather than in a file-specific context. * Users often use this entity to describe general remarks about the overall * concept proposed by the change. * <p> * <h5>PatchSetApproval</h5> * <p> * PatchSetApproval entities exist to fill in the <i>cells</i> of the approvals * table in the web UI. That is, a single PatchSetApproval record's key is the * tuple {@code (PatchSet,Account,ApprovalCategory)}. Each PatchSetApproval * carries with it a small score value, typically within the range -2..+2. * <p> * If an Account has created only PatchSetApprovals with a score value of 0, the * Change shows in their dashboard, and they are said to be CC'd (carbon copied) * on the Change, but are not a direct reviewer. This often happens when an * account was specified at upload time with the {@code --cc} command line flag, * or have published comments, but left the approval scores at 0 ("No Score"). * <p> * If an Account has one or more PatchSetApprovals with a score != 0, the Change * shows in their dashboard, and they are said to be an active reviewer. Such * individuals are highlighted when notice of a replacement patch set is sent, * or when notice of the change submission occurs. */ public final class Change { public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> { private static final long serialVersionUID = 1L; @Column(id = 1) protected int id; protected Id() { } public Id(final int id) { this.id = id; } @Override public int get() { return id; } @Override protected void set(int newValue) { id = newValue; } /** Parse a Change.Id out of a string representation. */ public static Id parse(final String str) { final Id r = new Id(); r.fromString(str); return r; } public static Id fromRef(final String ref) { return PatchSet.Id.fromRef(ref).getParentKey(); } } /** Globally unique identification of this change. */ public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> { private static final long serialVersionUID = 1L; @Column(id = 1, length = 60) protected String id; protected Key() { } public Key(final String id) { this.id = id; } @Override public String get() { return id; } @Override protected void set(String newValue) { id = newValue; } /** Construct a key that is after all keys prefixed by this key. */ public Key max() { final StringBuilder revEnd = new StringBuilder(get().length() + 1); revEnd.append(get()); revEnd.append('\u9fa5'); return new Key(revEnd.toString()); } /** Obtain a shorter version of this key string, using a leading prefix. */ public String abbreviate() { final String s = get(); return s.substring(0, Math.min(s.length(), 9)); } /** Parse a Change.Key out of a string representation. */ public static Key parse(final String str) { final Key r = new Key(); r.fromString(str); return r; } } /** Minimum database status constant for an open change. */ private static final char MIN_OPEN = 'a'; /** Database constant for {@link Status#NEW}. */ public static final char STATUS_NEW = 'n'; /** Database constant for {@link Status#SUBMITTED}. */ public static final char STATUS_SUBMITTED = 's'; /** Database constant for {@link Status#DRAFT}. */ public static final char STATUS_DRAFT = 'd'; /** Maximum database status constant for an open change. */ private static final char MAX_OPEN = 'z'; /** Database constant for {@link Status#MERGED}. */ public static final char STATUS_MERGED = 'M'; /** ID number of the first patch set in a change. */ public static final int INITIAL_PATCH_SET_ID = 1; /** * Current state within the basic workflow of the change. * * <p> * Within the database, lower case codes ('a'..'z') indicate a change that is * still open, and that can be modified/refined further, while upper case * codes ('A'..'Z') indicate a change that is closed and cannot be further * modified. * */ public static enum Status { /** * Change is open and pending review, or review is in progress. * * <p> * This is the default state assigned to a change when it is first created * in the database. A change stays in the NEW state throughout its review * cycle, until the change is submitted or abandoned. * * <p> * Changes in the NEW state can be moved to: * <ul> * <li>{@link #SUBMITTED} - when the Submit Patch Set action is used; * <li>{@link #ABANDONED} - when the Abandon action is used. * </ul> */ NEW(STATUS_NEW), /** * Change is open, but has been submitted to the merge queue. * * <p> * A change enters the SUBMITTED state when an authorized user presses the * "submit" action through the web UI, requesting that Gerrit merge the * change's current patch set into the destination branch. * * <p> * Typically a change resides in the SUBMITTED for only a brief sub-second * period while the merge queue fires and the destination branch is updated. * However, if a dependency commit (a {@link PatchSetAncestor}, directly or * transitively) is not yet merged into the branch, the change will hang in * the SUBMITTED state indefinitely. * * <p> * Changes in the SUBMITTED state can be moved to: * <ul> * <li>{@link #NEW} - when a replacement patch set is supplied, OR when a * merge conflict is detected; * <li>{@link #MERGED} - when the change has been successfully merged into * the destination branch; * <li>{@link #ABANDONED} - when the Abandon action is used. * </ul> */ SUBMITTED(STATUS_SUBMITTED), /** * Change is a draft change that only consists of draft patchsets. * * <p> * This is a change that is not meant to be submitted or reviewed yet. If * the uploader publishes the change, it becomes a NEW change. * Publishing is a one-way action, a change cannot return to DRAFT status. * Draft changes are only visible to the uploader and those explicitly * added as reviewers. * * <p> * Changes in the DRAFT state can be moved to: * <ul> * <li>{@link #NEW} - when the change is published, it becomes a new change; * </ul> */ DRAFT(STATUS_DRAFT), /** * Change is closed, and submitted to its destination branch. * * <p> * Once a change has been merged, it cannot be further modified by adding a * replacement patch set. Draft comments however may be published, * supporting a post-submit review. */ MERGED(STATUS_MERGED), /** * Change is closed, but was not submitted to its destination branch. * * <p> * Once a change has been abandoned, it cannot be further modified by adding * a replacement patch set, and it cannot be merged. Draft comments however * may be published, permitting reviewers to send constructive feedback. */ ABANDONED('A'); private final char code; private final boolean closed; private Status(final char c) { code = c; closed = !(MIN_OPEN <= c && c <= MAX_OPEN); } public char getCode() { return code; } public boolean isOpen() { return !closed; } public boolean isClosed() { return closed; } public static Status forCode(final char c) { for (final Status s : Status.values()) { if (s.code == c) { return s; } } return null; } } /** Locally assigned unique identifier of the change */ @Column(id = 1) protected Id changeId; /** Globally assigned unique identifier of the change */ @Column(id = 2) protected Key changeKey; /** optimistic locking */ @Column(id = 3) @RowVersion protected int rowVersion; /** When this change was first introduced into the database. */ @Column(id = 4) protected Timestamp createdOn; /** * When was a meaningful modification last made to this record's data * <p> * Note, this update timestamp includes its children. */ @Column(id = 5) protected Timestamp lastUpdatedOn; /** A {@link #lastUpdatedOn} ASC,{@link #changeId} ASC for sorting. */ @Column(id = 6, length = 16) protected String sortKey; @Column(id = 7, name = "owner_account_id") protected Account.Id owner; /** The branch (and project) this change merges into. */ @Column(id = 8) protected Branch.NameKey dest; /** Is the change currently open? Set to {@link #status}.isOpen(). */ @Column(id = 9) protected boolean open; /** Current state code; see {@link Status}. */ @Column(id = 10) protected char status; /** The current patch set. */ @Column(id = 12) protected int currentPatchSetId; /** Subject from the current patch set. */ @Column(id = 13) protected String subject; /** Topic name assigned by the user, if any. */ @Column(id = 14, notNull = false) protected String topic; /** * Null if the change has never been tested. * Empty if it has been tested but against a branch that does * not exist. */ @Column(id = 15, notNull = false) protected RevId lastSha1MergeTested; @Column(id = 16) protected boolean mergeable; protected Change() { } public Change(Change.Key newKey, Change.Id newId, Account.Id ownedBy, Branch.NameKey forBranch, Timestamp ts) { changeKey = newKey; changeId = newId; createdOn = ts; lastUpdatedOn = createdOn; owner = ownedBy; dest = forBranch; setStatus(Status.NEW); setLastSha1MergeTested(null); } public Change(Change other) { changeId = other.changeId; changeKey = other.changeKey; rowVersion = other.rowVersion; createdOn = other.createdOn; lastUpdatedOn = other.lastUpdatedOn; sortKey = other.sortKey; owner = other.owner; dest = other.dest; open = other.open; status = other.status; currentPatchSetId = other.currentPatchSetId; subject = other.subject; topic = other.topic; mergeable = other.mergeable; lastSha1MergeTested = other.lastSha1MergeTested; } /** Legacy 32 bit integer identity for a change. */ public Change.Id getId() { return changeId; } /** Legacy 32 bit integer identity for a change. */ public int getChangeId() { return changeId.get(); } /** The Change-Id tag out of the initial commit, or a natural key. */ public Change.Key getKey() { return changeKey; } public void setKey(final Change.Key k) { changeKey = k; } public Timestamp getCreatedOn() { return createdOn; } public Timestamp getLastUpdatedOn() { return lastUpdatedOn; } public void setLastUpdatedOn(Timestamp now) { lastUpdatedOn = now; } public int getRowVersion() { return rowVersion; } public String getSortKey() { return sortKey; } public void setSortKey(final String newSortKey) { sortKey = newSortKey; } public Account.Id getOwner() { return owner; } public Branch.NameKey getDest() { return dest; } public Project.NameKey getProject() { return dest.getParentKey(); } public String getSubject() { return subject; } /** Get the id of the most current {@link PatchSet} in this change. */ public PatchSet.Id currentPatchSetId() { if (currentPatchSetId > 0) { return new PatchSet.Id(changeId, currentPatchSetId); } return null; } public void setCurrentPatchSet(final PatchSetInfo ps) { currentPatchSetId = ps.getKey().get(); subject = ps.getSubject(); } public Status getStatus() { return Status.forCode(status); } public void setStatus(final Status newStatus) { open = newStatus.isOpen(); status = newStatus.getCode(); } public String getTopic() { return topic; } public void setTopic(String topic) { this.topic = topic; } public RevId getLastSha1MergeTested() { return lastSha1MergeTested; } public void setLastSha1MergeTested(RevId lastSha1MergeTested) { this.lastSha1MergeTested = lastSha1MergeTested; } public boolean isMergeable() { return mergeable; } public void setMergeable(boolean mergeable) { this.mergeable = mergeable; } }