summaryrefslogtreecommitdiffstats
path: root/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java')
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java530
1 files changed, 530 insertions, 0 deletions
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
new file mode 100644
index 0000000000..61e6a97e92
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -0,0 +1,530 @@
+// 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;
+ }
+}