blob: 61e6a97e923eb820471cc2e0f7d882836c45ded6 [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 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}: &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)
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';
/**
* 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 indefinately.
*
* <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 total number of {@link PatchSet} children in this Change. */
@Column(id = 11)
protected int 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;
/**
* 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(final Change.Key newKey, final Change.Id newId,
final Account.Id ownedBy, final Branch.NameKey forBranch) {
changeKey = newKey;
changeId = newId;
createdOn = new Timestamp(System.currentTimeMillis());
lastUpdatedOn = createdOn;
owner = ownedBy;
dest = forBranch;
setStatus(Status.NEW);
setLastSha1MergeTested(null);
}
/** 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 resetLastUpdatedOn() {
lastUpdatedOn = new Timestamp(System.currentTimeMillis());
}
public int getNumberOfPatchSets() {
return nbrPatchSets;
}
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();
}
/**
* Allocate a new PatchSet id within this change.
* <p>
* <b>Note: This makes the change dirty. Call update() after.</b>
*/
public void nextPatchSetId() {
++nbrPatchSets;
}
/**
* Reverts to an older PatchSet id within this change.
* <p>
* <b>Note: This makes the change dirty. Call update() after.</b>
*/
public void removeLastPatchSetId() {
--nbrPatchSets;
}
public PatchSet.Id currPatchSetId() {
return new PatchSet.Id(changeId, nbrPatchSets);
}
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;
}
}