blob: 2b1a7cfcc73b00417bcde4adfc083ff58b805824 [file] [log] [blame]
// 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 static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
import com.google.gerrit.extensions.client.ChangeStatus;
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;
import java.util.Arrays;
/**
* 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}: &quot;cover letter&quot; 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)
public 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(String ref) {
int cs = startIndex(ref);
if (cs < 0) {
return null;
}
int ce = nextNonDigit(ref, cs);
if (ref.substring(ce).equals(RefNames.META_SUFFIX)
|| PatchSet.Id.fromRef(ref, ce) >= 0) {
return new Change.Id(Integer.parseInt(ref.substring(cs, ce)));
}
return null;
}
static int startIndex(String ref) {
if (ref == null || !ref.startsWith(REFS_CHANGES)) {
return -1;
}
// Last 2 digits.
int ls = REFS_CHANGES.length();
int le = nextNonDigit(ref, ls);
if (le - ls != 2 || le >= ref.length() || ref.charAt(le) != '/') {
return -1;
}
// Change ID.
int cs = le + 1;
if (cs >= ref.length() || ref.charAt(cs) == '0') {
return -1;
}
int ce = nextNonDigit(ref, cs);
if (ce >= ref.length() || ref.charAt(ce) != '/') {
return -1;
}
switch (ce - cs) {
case 0:
return -1;
case 1:
if (ref.charAt(ls) != '0'
|| ref.charAt(ls + 1) != ref.charAt(cs)) {
return -1;
}
break;
default:
if (ref.charAt(ls) != ref.charAt(ce - 2)
|| ref.charAt(ls + 1) != ref.charAt(ce - 1)) {
return -1;
}
break;
}
return cs;
}
static int nextNonDigit(String s, int i) {
while (i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9') {
i++;
}
return i;
}
}
/** 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, ChangeStatus.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, ChangeStatus.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, ChangeStatus.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, ChangeStatus.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', ChangeStatus.ABANDONED);
static {
boolean ok = true;
if (Status.values().length != ChangeStatus.values().length) {
ok = false;
}
for (Status s : Status.values()) {
ok &= s.name().equals(s.changeStatus.name());
}
if (!ok) {
throw new IllegalStateException("Mismatched status mapping: "
+ Arrays.asList(Status.values()) + " != "
+ Arrays.asList(ChangeStatus.values()));
}
}
private final char code;
private final boolean closed;
private final ChangeStatus changeStatus;
private Status(char c, ChangeStatus cs) {
code = c;
closed = !(MIN_OPEN <= c && c <= MAX_OPEN);
changeStatus = cs;
}
public char getCode() {
return code;
}
public boolean isOpen() {
return !closed;
}
public boolean isClosed() {
return closed;
}
public ChangeStatus asChangeStatus() {
return changeStatus;
}
public static Status forCode(final char c) {
for (final Status s : Status.values()) {
if (s.code == c) {
return s;
}
}
return null;
}
public static Status forChangeStatus(ChangeStatus cs) {
for (Status s : Status.values()) {
if (s.changeStatus == cs) {
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;
// DELETED: id = 6 (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;
// DELETED: id = 9 (open)
/** Current state code; see {@link Status}. */
@Column(id = 10)
protected char status;
// DELETED: id = 11 (nbrPatchSets)
/** 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;
// DELETED: id = 15 (lastSha1MergeTested)
// DELETED: id = 16 (mergeable)
/**
* First line of first patch set's commit message.
*
* Unlike {@link #subject}, this string does not change if future patch sets
* change the first line.
*/
@Column(id = 17, notNull = false)
protected String originalSubject;
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);
}
public Change(Change other) {
changeId = other.changeId;
changeKey = other.changeKey;
rowVersion = other.rowVersion;
createdOn = other.createdOn;
lastUpdatedOn = other.lastUpdatedOn;
owner = other.owner;
dest = other.dest;
status = other.status;
currentPatchSetId = other.currentPatchSetId;
subject = other.subject;
originalSubject = other.originalSubject;
topic = other.topic;
}
/** 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 Account.Id getOwner() {
return owner;
}
public Branch.NameKey getDest() {
return dest;
}
public Project.NameKey getProject() {
return dest.getParentKey();
}
public String getSubject() {
return subject;
}
public String getOriginalSubject() {
return originalSubject != null ? originalSubject : 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) {
if (originalSubject == null && subject != null) {
// Change was created before schema upgrade. Use the last subject
// associated with this change, as the most recent discussion will
// be under that thread in an email client such as GMail.
originalSubject = subject;
}
currentPatchSetId = ps.getKey().get();
subject = ps.getSubject();
if (originalSubject == null) {
// Newly created changes remember the first commit's subject.
originalSubject = subject;
}
}
public Status getStatus() {
return Status.forCode(status);
}
public void setStatus(Status newStatus) {
status = newStatus.getCode();
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
@Override
public String toString() {
return new StringBuilder(getClass().getSimpleName())
.append('{').append(changeId)
.append(" (").append(changeKey).append("), ")
.append("dest=").append(dest).append(", ")
.append("status=").append(status).append('}')
.toString();
}
}