All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.reviewdb.client.Change Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// 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}.
 *
 * 

The data graph rooted below a Change can be quite complex: * *

 *   {@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 PatchLineComment}: comment about a specific line
 * 
* *

* *

PatchSets
* *

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. * *

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. * *

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. * *

When additional PatchSets appear under a change, these PatchSets reference replacement * commits; alternative commits that could be made to the project instead of the original commit * referenced by the first PatchSet. * *

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. * *

* *

ChangeMessage
* *

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. * *

* *

PatchSetApproval
* *

PatchSetApproval entities exist to fill in the cells 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. * *

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"). * *

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> { private static final long serialVersionUID = 1L; @Column(id = 1) public int id; protected Id() {} public Id(int id) { this.id = id; } @Override public int get() { return id; } @Override protected void set(int newValue) { id = newValue; } public String toRefPrefix() { return refPrefixBuilder().toString(); } StringBuilder refPrefixBuilder() { StringBuilder r = new StringBuilder(32).append(REFS_CHANGES); int m = id % 100; if (m < 10) { r.append('0'); } return r.append(m).append('/').append(id).append('/'); } /** Parse a Change.Id out of a string representation. */ public static Id parse(String str) { final Id r = new Id(); r.fromString(str); return r; } public static Id fromRef(String ref) { if (RefNames.isRefsEdit(ref)) { return fromEditRefPart(ref); } int cs = startIndex(ref); if (cs < 0) { return null; } int ce = nextNonDigit(ref, cs); if (ref.substring(ce).equals(RefNames.META_SUFFIX) || ref.substring(ce).equals(RefNames.ROBOT_COMMENTS_SUFFIX) || PatchSet.Id.fromRef(ref, ce) >= 0) { return new Change.Id(Integer.parseInt(ref.substring(cs, ce))); } return null; } public static Id fromAllUsersRef(String ref) { if (ref == null) { return null; } String prefix; if (ref.startsWith(RefNames.REFS_STARRED_CHANGES)) { prefix = RefNames.REFS_STARRED_CHANGES; } else if (ref.startsWith(RefNames.REFS_DRAFT_COMMENTS)) { prefix = RefNames.REFS_DRAFT_COMMENTS; } else { return null; } int cs = startIndex(ref, prefix); if (cs < 0) { return null; } int ce = nextNonDigit(ref, cs); if (ce < ref.length() && ref.charAt(ce) == '/' && isNumeric(ref, ce + 1)) { return new Change.Id(Integer.parseInt(ref.substring(cs, ce))); } return null; } private static boolean isNumeric(String s, int off) { if (off >= s.length()) { return false; } for (int i = off; i < s.length(); i++) { if (!Character.isDigit(s.charAt(i))) { return false; } } return true; } public static Id fromEditRefPart(String ref) { int startChangeId = ref.indexOf(RefNames.EDIT_PREFIX) + RefNames.EDIT_PREFIX.length(); int endChangeId = nextNonDigit(ref, startChangeId); String id = ref.substring(startChangeId, endChangeId); if (id != null && !id.isEmpty()) { return new Change.Id(Integer.parseInt(id)); } return null; } public static Id fromRefPart(String ref) { Integer id = RefNames.parseShardedRefPart(ref); return id != null ? new Change.Id(id) : null; } static int startIndex(String ref) { return startIndex(ref, REFS_CHANGES); } static int startIndex(String ref, String expectedPrefix) { if (ref == null || !ref.startsWith(expectedPrefix)) { return -1; } // Last 2 digits. int ls = expectedPrefix.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. This generally takes the form of a string * "Ixxxxxx...", and is stored in the Change-Id footer of a commit. */ public static class Key extends StringKey> { private static final long serialVersionUID = 1L; @Column(id = 1, length = 60) protected String id; protected Key() {} public Key(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(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'; /** 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; /** Change-Id pattern. */ public static final String CHANGE_ID_PATTERN = "^[iI][0-9a-f]{4,}.*$"; /** * Current state within the basic workflow of the change. * *

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 enum Status { /** * Change is open and pending review, or review is in progress. * *

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. * *

Changes in the NEW state can be moved to: * *

    *
  • {@link #MERGED} - when the Submit Patch Set action is used; *
  • {@link #ABANDONED} - when the Abandon action is used. *
*/ NEW(STATUS_NEW, ChangeStatus.NEW), /** * Change is closed, and submitted to its destination branch. * *

Once a change has been merged, it cannot be further modified by adding a replacement patch */ MERGED(STATUS_MERGED, ChangeStatus.MERGED), /** * Change is closed, but was not submitted to its destination branch. * *

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; 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(char c) { for (Status s : Status.values()) { if (s.code == c) { return s; } } // TODO(davido): Remove in 3.0, after all sites upgraded to version, // where DRAFT status was removed. This code path is still needed, // when changes are deserialized from the secondary index, during // the online migration to the new schema version wasn't completed. if (c == 'd') { return Status.NEW; } 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 * *

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; /** * Unique id for the changes submitted together assigned during merging. Only set if the status is * MERGED. */ @Column(id = 18, notNull = false) protected String submissionId; /** Allows assigning a change to a user. */ @Column(id = 19, notNull = false) protected Account.Id assignee; /** Whether the change is private. */ @Column(id = 20) protected boolean isPrivate; /** Whether the change is work in progress. */ @Column(id = 21) protected boolean workInProgress; /** Whether the change has started review. */ @Column(id = 22) protected boolean reviewStarted; /** References a change that this change reverts. */ @Column(id = 23, notNull = false) protected Id revertOf; /** @see com.google.gerrit.server.notedb.NoteDbChangeState */ @Column(id = 101, notNull = false, length = Integer.MAX_VALUE) protected String noteDbState; 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) { assignee = other.assignee; 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; submissionId = other.submissionId; topic = other.topic; isPrivate = other.isPrivate; workInProgress = other.workInProgress; reviewStarted = other.reviewStarted; noteDbState = other.noteDbState; revertOf = other.revertOf; } /** 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(Change.Key k) { changeKey = k; } public Account.Id getAssignee() { return assignee; } public void setAssignee(Account.Id a) { assignee = a; } public Timestamp getCreatedOn() { return createdOn; } public void setCreatedOn(Timestamp ts) { createdOn = ts; } public Timestamp getLastUpdatedOn() { return lastUpdatedOn; } public void setLastUpdatedOn(Timestamp now) { lastUpdatedOn = now; } public int getRowVersion() { return rowVersion; } public Account.Id getOwner() { return owner; } public void setOwner(Account.Id owner) { this.owner = owner; } public Branch.NameKey getDest() { return dest; } public void setDest(Branch.NameKey dest) { this.dest = dest; } public Project.NameKey getProject() { return dest.getParentKey(); } public String getSubject() { return subject; } public String getOriginalSubject() { return originalSubject != null ? originalSubject : subject; } public String getOriginalSubjectOrNull() { return originalSubject; } /** 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(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 void setCurrentPatchSet(PatchSet.Id psId, String subject, String originalSubject) { if (!psId.getParentKey().equals(changeId)) { throw new IllegalArgumentException("patch set ID " + psId + " is not for change " + changeId); } currentPatchSetId = psId.get(); this.subject = subject; this.originalSubject = originalSubject; } public void clearCurrentPatchSet() { currentPatchSetId = 0; subject = null; originalSubject = null; } public String getSubmissionId() { return submissionId; } public void setSubmissionId(String id) { this.submissionId = id; } 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; } public boolean isPrivate() { return isPrivate; } public void setPrivate(boolean isPrivate) { this.isPrivate = isPrivate; } public boolean isWorkInProgress() { return workInProgress; } public void setWorkInProgress(boolean workInProgress) { this.workInProgress = workInProgress; } public boolean hasReviewStarted() { return reviewStarted; } public void setReviewStarted(boolean reviewStarted) { this.reviewStarted = reviewStarted; } public void setRevertOf(Id revertOf) { this.revertOf = revertOf; } public Id getRevertOf() { return this.revertOf; } public String getNoteDbState() { return noteDbState; } public void setNoteDbState(String state) { noteDbState = state; } @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(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy