Line data Source code
1 : // Copyright (C) 2008 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.entities;
16 :
17 : import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
18 :
19 : import com.google.auto.value.AutoValue;
20 : import com.google.common.primitives.Ints;
21 : import com.google.gerrit.common.Nullable;
22 : import com.google.gerrit.extensions.client.ChangeStatus;
23 : import com.google.gson.Gson;
24 : import com.google.gson.TypeAdapter;
25 : import com.google.gson.annotations.SerializedName;
26 : import java.time.Instant;
27 : import java.util.Arrays;
28 : import java.util.Optional;
29 :
30 : /**
31 : * A change proposed to be merged into a branch.
32 : *
33 : * <p>The data graph rooted below a Change can be quite complex:
34 : *
35 : * <pre>
36 : * {@link Change}
37 : * |
38 : * +- {@link ChangeMessage}: "cover letter" or general comment.
39 : * |
40 : * +- {@link PatchSet}: a single variant of this change.
41 : * |
42 : * +- {@link PatchSetApproval}: a +/- vote on the change's current state.
43 : * |
44 : * +- {@link HumanComment}: comment about a specific line
45 : * </pre>
46 : *
47 : * <p>
48 : *
49 : * <h5>PatchSets</h5>
50 : *
51 : * <p>Every change has at least one PatchSet. A change starts out with one PatchSet, the initial
52 : * proposal put forth by the change owner. This {@link Account} is usually also listed as the author
53 : * and committer in the PatchSetInfo.
54 : *
55 : * <p>Each PatchSet contains zero or more Patch records, detailing the file paths impacted by the
56 : * change (otherwise known as, the file paths the author added/deleted/modified). Sometimes a merge
57 : * commit can contain zero patches, if the merge has no conflicts, or has no impact other than to
58 : * cut off a line of development.
59 : *
60 : * <p>Each Comment is a draft or a published comment about a single line of the associated file.
61 : * These are the inline comment entities created by users as they perform a review.
62 : *
63 : * <p>When additional PatchSets appear under a change, these PatchSets reference <i>replacement</i>
64 : * commits; alternative commits that could be made to the project instead of the original commit
65 : * referenced by the first PatchSet.
66 : *
67 : * <p>A change has at most one current PatchSet. The current PatchSet is updated when a new
68 : * replacement PatchSet is uploaded. When a change is submitted, the current patch set is what is
69 : * merged into the destination branch.
70 : *
71 : * <p>
72 : *
73 : * <h5>ChangeMessage</h5>
74 : *
75 : * <p>The ChangeMessage entity is a general free-form comment about the whole change, rather than
76 : * Comment's file and line specific context. The ChangeMessage appears at the start of any email
77 : * generated by Gerrit, and is shown on the change overview page, rather than in a file-specific
78 : * context. Users often use this entity to describe general remarks about the overall concept
79 : * proposed by the change.
80 : *
81 : * <p>
82 : *
83 : * <h5>PatchSetApproval</h5>
84 : *
85 : * <p>PatchSetApproval entities exist to fill in the <i>cells</i> of the approvals table in the web
86 : * UI. That is, a single PatchSetApproval record's key is the tuple {@code
87 : * (PatchSet,Account,ApprovalCategory)}. Each PatchSetApproval carries with it a small score value,
88 : * typically within the range -2..+2.
89 : *
90 : * <p>If an Account has created only PatchSetApprovals with a score value of 0, the Change shows in
91 : * their dashboard, and they are said to be CC'd (carbon copied) on the Change, but are not a direct
92 : * reviewer. This often happens when an account was specified at upload time with the {@code --cc}
93 : * command line flag, or have published comments, but left the approval scores at 0 ("No Score").
94 : *
95 : * <p>If an Account has one or more PatchSetApprovals with a score != 0, the Change shows in their
96 : * dashboard, and they are said to be an active reviewer. Such individuals are highlighted when
97 : * notice of a replacement patch set is sent, or when notice of the change submission occurs.
98 : */
99 : public final class Change {
100 :
101 : public static Id id(int id) {
102 109 : return new AutoValue_Change_Id(id);
103 : }
104 :
105 : /** The numeric change ID */
106 : @AutoValue
107 109 : public abstract static class Id {
108 : /**
109 : * Parse a Change.Id out of a string representation.
110 : *
111 : * @param str the string to parse
112 : * @return Optional containing the Change.Id, or {@code Optional.empty()} if str does not
113 : * represent a valid Change.Id.
114 : */
115 : public static Optional<Id> tryParse(String str) {
116 11 : Integer id = Ints.tryParse(str);
117 11 : return id != null ? Optional.of(Change.id(id)) : Optional.empty();
118 : }
119 :
120 : @Nullable
121 : public static Id fromRef(String ref) {
122 114 : if (RefNames.isRefsEdit(ref)) {
123 21 : return fromEditRefPart(ref);
124 : }
125 114 : int cs = startIndex(ref);
126 114 : if (cs < 0) {
127 110 : return null;
128 : }
129 100 : int ce = nextNonDigit(ref, cs);
130 100 : if (ref.substring(ce).equals(RefNames.META_SUFFIX)
131 100 : || ref.substring(ce).equals(RefNames.ROBOT_COMMENTS_SUFFIX)
132 100 : || PatchSet.Id.fromRef(ref, ce) >= 0) {
133 100 : return Change.id(Integer.parseInt(ref.substring(cs, ce)));
134 : }
135 1 : return null;
136 : }
137 :
138 : @Nullable
139 : public static Id fromAllUsersRef(String ref) {
140 9 : if (ref == null) {
141 1 : return null;
142 : }
143 : String prefix;
144 9 : if (ref.startsWith(RefNames.REFS_STARRED_CHANGES)) {
145 5 : prefix = RefNames.REFS_STARRED_CHANGES;
146 9 : } else if (ref.startsWith(RefNames.REFS_DRAFT_COMMENTS)) {
147 9 : prefix = RefNames.REFS_DRAFT_COMMENTS;
148 : } else {
149 1 : return null;
150 : }
151 9 : int cs = startIndex(ref, prefix);
152 9 : if (cs < 0) {
153 1 : return null;
154 : }
155 9 : int ce = nextNonDigit(ref, cs);
156 9 : if (ce < ref.length() && ref.charAt(ce) == '/' && isNumeric(ref, ce + 1)) {
157 9 : return Change.id(Integer.parseInt(ref.substring(cs, ce)));
158 : }
159 1 : return null;
160 : }
161 :
162 : private static boolean isNumeric(String s, int off) {
163 9 : if (off >= s.length()) {
164 1 : return false;
165 : }
166 9 : for (int i = off; i < s.length(); i++) {
167 9 : if (!Character.isDigit(s.charAt(i))) {
168 1 : return false;
169 : }
170 : }
171 9 : return true;
172 : }
173 :
174 : @Nullable
175 : public static Id fromEditRefPart(String ref) {
176 28 : int startChangeId = ref.indexOf(RefNames.EDIT_PREFIX) + RefNames.EDIT_PREFIX.length();
177 28 : int endChangeId = nextNonDigit(ref, startChangeId);
178 28 : String id = ref.substring(startChangeId, endChangeId);
179 28 : if (id != null && !id.isEmpty()) {
180 28 : return Change.id(Integer.parseInt(id));
181 : }
182 0 : return null;
183 : }
184 :
185 : @Nullable
186 : public static Id fromRefPart(String ref) {
187 40 : Integer id = RefNames.parseShardedRefPart(ref);
188 40 : return id != null ? Change.id(id) : null;
189 : }
190 :
191 : static int startIndex(String ref) {
192 115 : return startIndex(ref, REFS_CHANGES);
193 : }
194 :
195 : static int startIndex(String ref, String expectedPrefix) {
196 115 : if (ref == null || !ref.startsWith(expectedPrefix)) {
197 110 : return -1;
198 : }
199 :
200 : // Last 2 digits.
201 101 : int ls = expectedPrefix.length();
202 101 : int le = nextNonDigit(ref, ls);
203 101 : if (le - ls != 2 || le >= ref.length() || ref.charAt(le) != '/') {
204 1 : return -1;
205 : }
206 :
207 : // Change ID.
208 101 : int cs = le + 1;
209 101 : if (cs >= ref.length() || ref.charAt(cs) == '0') {
210 1 : return -1;
211 : }
212 101 : int ce = nextNonDigit(ref, cs);
213 101 : if (ce >= ref.length() || ref.charAt(ce) != '/') {
214 1 : return -1;
215 : }
216 101 : switch (ce - cs) {
217 : case 0:
218 1 : return -1;
219 : case 1:
220 101 : if (ref.charAt(ls) != '0' || ref.charAt(ls + 1) != ref.charAt(cs)) {
221 0 : return -1;
222 : }
223 : break;
224 : default:
225 64 : if (ref.charAt(ls) != ref.charAt(ce - 2) || ref.charAt(ls + 1) != ref.charAt(ce - 1)) {
226 1 : return -1;
227 : }
228 : break;
229 : }
230 101 : return cs;
231 : }
232 :
233 : static int nextNonDigit(String s, int i) {
234 101 : while (i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9') {
235 101 : i++;
236 : }
237 101 : return i;
238 : }
239 :
240 : abstract int id();
241 :
242 : public int get() {
243 107 : return id();
244 : }
245 :
246 : public String toRefPrefix() {
247 104 : return refPrefixBuilder().toString();
248 : }
249 :
250 : StringBuilder refPrefixBuilder() {
251 104 : StringBuilder r = new StringBuilder(32).append(REFS_CHANGES);
252 104 : int m = get() % 100;
253 104 : if (m < 10) {
254 104 : r.append('0');
255 : }
256 104 : return r.append(m).append('/').append(get()).append('/');
257 : }
258 :
259 : @Override
260 : public final String toString() {
261 105 : return Integer.toString(get());
262 : }
263 : }
264 :
265 : public static Key key(String key) {
266 112 : return new AutoValue_Change_Key(key);
267 : }
268 :
269 : /**
270 : * Globally unique identification of this change. This generally takes the form of a string
271 : * "Ixxxxxx...", and is stored in the Change-Id footer of a commit.
272 : */
273 : @AutoValue
274 112 : public abstract static class Key {
275 : // TODO(dborowitz): This hardly seems worth it: why would someone pass a URL-encoded change key?
276 : // Ideally the standard key() factory method would enforce the format and throw IAE.
277 : public static Key parse(String str) {
278 0 : return Change.key(KeyUtil.decode(str));
279 : }
280 :
281 : @SerializedName("id")
282 : abstract String key();
283 :
284 : public String get() {
285 107 : return key();
286 : }
287 :
288 : /** Construct a key that is after all keys prefixed by this key. */
289 : public Key max() {
290 0 : final StringBuilder revEnd = new StringBuilder(get().length() + 1);
291 0 : revEnd.append(get());
292 0 : revEnd.append('\u9fa5');
293 0 : return Change.key(revEnd.toString());
294 : }
295 :
296 : /** Obtain a shorter version of this key string, using a leading prefix. */
297 : public String abbreviate() {
298 4 : final String s = get();
299 4 : return s.substring(0, Math.min(s.length(), 9));
300 : }
301 :
302 : @Override
303 : public final String toString() {
304 11 : return get();
305 : }
306 :
307 : public static TypeAdapter<Key> typeAdapter(Gson gson) {
308 2 : return new AutoValue_Change_Key.GsonTypeAdapter(gson);
309 : }
310 : }
311 :
312 : /** Minimum database status constant for an open change. */
313 : private static final char MIN_OPEN = 'a';
314 : /** Database constant for {@link Status#NEW}. */
315 : public static final char STATUS_NEW = 'n';
316 : /** Maximum database status constant for an open change. */
317 : private static final char MAX_OPEN = 'z';
318 :
319 : /** Database constant for {@link Status#MERGED}. */
320 : public static final char STATUS_MERGED = 'M';
321 :
322 : /** ID number of the first patch set in a change. */
323 : public static final int INITIAL_PATCH_SET_ID = 1;
324 :
325 : /** Change-Id pattern. */
326 : public static final String CHANGE_ID_PATTERN = "^[iI][0-9a-f]{4,}.*$";
327 :
328 : /**
329 : * Current state within the basic workflow of the change.
330 : *
331 : * <p>Within the database, lower case codes ('a'..'z') indicate a change that is still open, and
332 : * that can be modified/refined further, while upper case codes ('A'..'Z') indicate a change that
333 : * is closed and cannot be further modified.
334 : */
335 153 : public enum Status {
336 : /**
337 : * Change is open and pending review, or review is in progress.
338 : *
339 : * <p>This is the default state assigned to a change when it is first created in the database. A
340 : * change stays in the NEW state throughout its review cycle, until the change is submitted or
341 : * abandoned.
342 : *
343 : * <p>Changes in the NEW state can be moved to:
344 : *
345 : * <ul>
346 : * <li>{@link #MERGED} - when the Submit Patch Set action is used;
347 : * <li>{@link #ABANDONED} - when the Abandon action is used.
348 : * </ul>
349 : */
350 153 : NEW(STATUS_NEW, ChangeStatus.NEW),
351 :
352 : /**
353 : * Change is closed, and submitted to its destination branch.
354 : *
355 : * <p>Once a change has been merged, it cannot be further modified by adding a replacement patch
356 : */
357 153 : MERGED(STATUS_MERGED, ChangeStatus.MERGED),
358 :
359 : /**
360 : * Change is closed, but was not submitted to its destination branch.
361 : *
362 : * <p>Once a change has been abandoned, it cannot be further modified by adding a replacement
363 : * patch set, and it cannot be merged. Draft comments however may be published, permitting
364 : * reviewers to send constructive feedback.
365 : */
366 153 : ABANDONED('A', ChangeStatus.ABANDONED);
367 :
368 : static {
369 153 : boolean ok = true;
370 153 : if (Status.values().length != ChangeStatus.values().length) {
371 0 : ok = false;
372 : }
373 153 : for (Status s : Status.values()) {
374 153 : ok &= s.name().equals(s.changeStatus.name());
375 : }
376 153 : if (!ok) {
377 0 : throw new IllegalStateException(
378 : "Mismatched status mapping: "
379 0 : + Arrays.asList(Status.values())
380 : + " != "
381 0 : + Arrays.asList(ChangeStatus.values()));
382 : }
383 153 : }
384 :
385 : private final char code;
386 : private final boolean closed;
387 : private final ChangeStatus changeStatus;
388 :
389 153 : Status(char c, ChangeStatus cs) {
390 153 : code = c;
391 153 : closed = !(MIN_OPEN <= c && c <= MAX_OPEN);
392 153 : changeStatus = cs;
393 153 : }
394 :
395 : public char getCode() {
396 106 : return code;
397 : }
398 :
399 : public boolean isOpen() {
400 150 : return !closed;
401 : }
402 :
403 : public boolean isClosed() {
404 103 : return closed;
405 : }
406 :
407 : public ChangeStatus asChangeStatus() {
408 103 : return changeStatus;
409 : }
410 :
411 : @Nullable
412 : public static Status forCode(char c) {
413 105 : for (Status s : Status.values()) {
414 105 : if (s.code == c) {
415 105 : return s;
416 : }
417 : }
418 :
419 1 : return null;
420 : }
421 :
422 : @Nullable
423 : public static Status forChangeStatus(ChangeStatus cs) {
424 1 : for (Status s : Status.values()) {
425 1 : if (s.changeStatus == cs) {
426 1 : return s;
427 : }
428 : }
429 0 : return null;
430 : }
431 : }
432 :
433 : /** Locally assigned unique identifier of the change */
434 : private Id changeId;
435 :
436 : /** Globally assigned unique identifier of the change */
437 : private Key changeKey;
438 :
439 : /** When this change was first introduced into the database. */
440 : private Instant createdOn;
441 :
442 : /**
443 : * When was a meaningful modification last made to this record's data
444 : *
445 : * <p>Note, this update timestamp includes its children.
446 : */
447 : private Instant lastUpdatedOn;
448 :
449 : private Account.Id owner;
450 :
451 : /** The branch (and project) this change merges into. */
452 : private BranchNameKey dest;
453 :
454 : /** Current state code; see {@link Status}. */
455 : private char status;
456 :
457 : /** The current patch set. */
458 : private int currentPatchSetId;
459 :
460 : /** Subject from the current patch set. */
461 : private String subject;
462 :
463 : /** Topic name assigned by the user, if any. */
464 : @Nullable private String topic;
465 :
466 : /**
467 : * First line of first patch set's commit message.
468 : *
469 : * <p>Unlike {@link #subject}, this string does not change if future patch sets change the first
470 : * line.
471 : */
472 : @Nullable private String originalSubject;
473 :
474 : /**
475 : * Unique id for the changes submitted together assigned during merging. Only set if the status is
476 : * MERGED.
477 : */
478 : @Nullable private String submissionId;
479 :
480 : /** Allows assigning a change to a user. */
481 : @Nullable private Account.Id assignee;
482 :
483 : /** Whether the change is private. */
484 : private boolean isPrivate;
485 :
486 : /** Whether the change is work in progress. */
487 : private boolean workInProgress;
488 :
489 : /** Whether the change has started review. */
490 : private boolean reviewStarted;
491 :
492 : /** References a change that this change reverts. */
493 : @Nullable private Id revertOf;
494 :
495 : /** References the source change and patchset that this change was cherry-picked from. */
496 : @Nullable private PatchSet.Id cherryPickOf;
497 :
498 0 : Change() {}
499 :
500 : public Change(
501 106 : Change.Key newKey, Change.Id newId, Account.Id ownedBy, BranchNameKey forBranch, Instant ts) {
502 106 : changeKey = newKey;
503 106 : changeId = newId;
504 106 : createdOn = ts;
505 106 : lastUpdatedOn = createdOn;
506 106 : owner = ownedBy;
507 106 : dest = forBranch;
508 106 : setStatus(Status.NEW);
509 106 : }
510 :
511 103 : public Change(Change other) {
512 103 : assignee = other.assignee;
513 103 : changeId = other.changeId;
514 103 : changeKey = other.changeKey;
515 103 : createdOn = other.createdOn;
516 103 : lastUpdatedOn = other.lastUpdatedOn;
517 103 : owner = other.owner;
518 103 : dest = other.dest;
519 103 : status = other.status;
520 103 : currentPatchSetId = other.currentPatchSetId;
521 103 : subject = other.subject;
522 103 : originalSubject = other.originalSubject;
523 103 : submissionId = other.submissionId;
524 103 : topic = other.topic;
525 103 : isPrivate = other.isPrivate;
526 103 : workInProgress = other.workInProgress;
527 103 : reviewStarted = other.reviewStarted;
528 103 : revertOf = other.revertOf;
529 103 : cherryPickOf = other.cherryPickOf;
530 103 : }
531 :
532 : /** 32 bit integer identity for a change. */
533 : public Change.Id getId() {
534 106 : return changeId;
535 : }
536 :
537 : /** 32 bit integer identity for a change. */
538 : public int getChangeId() {
539 104 : return changeId.get();
540 : }
541 :
542 : /** The Change-Id tag out of the initial commit, or a natural key. */
543 : public Change.Key getKey() {
544 105 : return changeKey;
545 : }
546 :
547 : public void setKey(Change.Key k) {
548 103 : changeKey = k;
549 103 : }
550 :
551 : public Account.Id getAssignee() {
552 104 : return assignee;
553 : }
554 :
555 : public void setAssignee(Account.Id a) {
556 8 : assignee = a;
557 8 : }
558 :
559 : public Instant getCreatedOn() {
560 105 : return createdOn;
561 : }
562 :
563 : public void setCreatedOn(Instant ts) {
564 103 : createdOn = ts;
565 103 : }
566 :
567 : public Instant getLastUpdatedOn() {
568 104 : return lastUpdatedOn;
569 : }
570 :
571 : public void setLastUpdatedOn(Instant now) {
572 104 : lastUpdatedOn = now;
573 104 : }
574 :
575 : public Account.Id getOwner() {
576 104 : return owner;
577 : }
578 :
579 : public void setOwner(Account.Id owner) {
580 103 : this.owner = owner;
581 103 : }
582 :
583 : public BranchNameKey getDest() {
584 105 : return dest;
585 : }
586 :
587 : public void setDest(BranchNameKey dest) {
588 103 : this.dest = dest;
589 103 : }
590 :
591 : public Project.NameKey getProject() {
592 104 : return dest.project();
593 : }
594 :
595 : public String getSubject() {
596 105 : return subject;
597 : }
598 :
599 : public String getOriginalSubject() {
600 104 : return originalSubject != null ? originalSubject : subject;
601 : }
602 :
603 : public String getOriginalSubjectOrNull() {
604 104 : return originalSubject;
605 : }
606 :
607 : /** Get the id of the most current {@link PatchSet} in this change. */
608 : @Nullable
609 : public PatchSet.Id currentPatchSetId() {
610 105 : if (currentPatchSetId > 0) {
611 105 : return PatchSet.id(changeId, currentPatchSetId);
612 : }
613 5 : return null;
614 : }
615 :
616 : public void setCurrentPatchSet(PatchSetInfo ps) {
617 104 : if (originalSubject == null && subject != null) {
618 : // Change was created before schema upgrade. Use the last subject
619 : // associated with this change, as the most recent discussion will
620 : // be under that thread in an email client such as GMail.
621 0 : originalSubject = subject;
622 : }
623 :
624 104 : currentPatchSetId = ps.getKey().get();
625 104 : subject = ps.getSubject();
626 :
627 104 : if (originalSubject == null) {
628 : // Newly created changes remember the first commit's subject.
629 104 : originalSubject = subject;
630 : }
631 104 : }
632 :
633 : public void setCurrentPatchSet(PatchSet.Id psId, String subject, String originalSubject) {
634 104 : if (!psId.changeId().equals(changeId)) {
635 0 : throw new IllegalArgumentException("patch set ID " + psId + " is not for change " + changeId);
636 : }
637 104 : currentPatchSetId = psId.get();
638 104 : this.subject = subject;
639 104 : this.originalSubject = originalSubject;
640 104 : }
641 :
642 : public void clearCurrentPatchSet() {
643 0 : currentPatchSetId = 0;
644 0 : subject = null;
645 0 : originalSubject = null;
646 0 : }
647 :
648 : public String getSubmissionId() {
649 104 : return submissionId;
650 : }
651 :
652 : public void setSubmissionId(String id) {
653 104 : this.submissionId = id;
654 104 : }
655 :
656 : public Status getStatus() {
657 105 : return Status.forCode(status);
658 : }
659 :
660 : public void setStatus(Status newStatus) {
661 106 : status = newStatus.getCode();
662 106 : }
663 :
664 : public boolean isNew() {
665 104 : return getStatus().equals(Status.NEW);
666 : }
667 :
668 : public boolean isMerged() {
669 103 : return getStatus().equals(Status.MERGED);
670 : }
671 :
672 : public boolean isAbandoned() {
673 103 : return getStatus().equals(Status.ABANDONED);
674 : }
675 :
676 : public boolean isClosed() {
677 103 : return isAbandoned() || isMerged();
678 : }
679 :
680 : public String getTopic() {
681 105 : return topic;
682 : }
683 :
684 : public void setTopic(String topic) {
685 104 : this.topic = topic;
686 104 : }
687 :
688 : public boolean isPrivate() {
689 105 : return isPrivate;
690 : }
691 :
692 : public void setPrivate(boolean isPrivate) {
693 104 : this.isPrivate = isPrivate;
694 104 : }
695 :
696 : public boolean isWorkInProgress() {
697 105 : return workInProgress;
698 : }
699 :
700 : public void setWorkInProgress(boolean workInProgress) {
701 104 : this.workInProgress = workInProgress;
702 104 : }
703 :
704 : public boolean hasReviewStarted() {
705 104 : return reviewStarted;
706 : }
707 :
708 : public void setReviewStarted(boolean reviewStarted) {
709 104 : this.reviewStarted = reviewStarted;
710 104 : }
711 :
712 : public void setRevertOf(Id revertOf) {
713 104 : this.revertOf = revertOf;
714 104 : }
715 :
716 : public Id getRevertOf() {
717 104 : return this.revertOf;
718 : }
719 :
720 : public PatchSet.Id getCherryPickOf() {
721 104 : return cherryPickOf;
722 : }
723 :
724 : public void setCherryPickOf(@Nullable PatchSet.Id cherryPickOf) {
725 103 : this.cherryPickOf = cherryPickOf;
726 103 : }
727 :
728 : @Override
729 : public String toString() {
730 5 : return new StringBuilder(getClass().getSimpleName())
731 5 : .append('{')
732 5 : .append(changeId)
733 5 : .append(" (")
734 5 : .append(changeKey)
735 5 : .append("), ")
736 5 : .append("dest=")
737 5 : .append(dest)
738 5 : .append(", ")
739 5 : .append("status=")
740 5 : .append(status)
741 5 : .append('}')
742 5 : .toString();
743 : }
744 : }
|