Add 'CherryPickOf' field for a change
After a change is created or updated using the 'cherry-pick'
functionality, this field will contain the source change number
and the patchset. Having this field helps us identify changes
where actual dev time was spent on by filtering out propagated
changes. This is especially useful for organizations wanting to
generate cost metrics.
Change-Id: I782a56aa52c52670ec74fabb713fe47ecba24de1
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 6f55398..9dd58b8 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -453,6 +453,15 @@
Abandoned changes can be link:user-review-ui.html#restore[restored] if
later they are needed again.
+[[cherrypickof]]
+== Cherry-Pick changes of a Change
+
+When a change is created/updated using the 'cherry-pick' functionalty,
+the original change and patchset details are recorded in the Change's
+cherrypick field. This field cannot be set or updated by the user in
+any way. It is set automatically after the cherry-pick operation completes
+successfully.
+
[[topics]]
== Using Topics
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index aa5edf0..359c32a 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -229,6 +229,16 @@
Changes whose link:intro-user.html#hashtags[hashtag] matches 'HASHTAG'.
The match is case-insensitive.
+[[cherrypickof]]
+cherrypickof:'CHANGE[,PATCHSET]'::
++
+Changes which were created using the 'cherry-pick' functionality and
+whose source change number matches 'CHANGE' and source patchset number
+matches 'PATCHSET'. Note that 'PATCHSET' is optional. For example, a
+`cherrypickof:12345` matches all changes which were cherry-picked from
+change 12345 and `cherrypickof:12345,2` matches all changes which were
+cherry-picked from the 2nd patchset of change 12345.
+
[[ref]]
ref:'REF'::
+
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index 04e97dc..c768094 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -531,6 +531,9 @@
/** References a change that this change reverts. */
@Nullable protected Id revertOf;
+ /** References the source change and patchset that this change was cherry-picked from. */
+ @Nullable protected PatchSet.Id cherryPickOf;
+
protected Change() {}
public Change(
@@ -567,6 +570,7 @@
workInProgress = other.workInProgress;
reviewStarted = other.reviewStarted;
revertOf = other.revertOf;
+ cherryPickOf = other.cherryPickOf;
}
/** Legacy 32 bit integer identity for a change. */
@@ -760,6 +764,14 @@
return this.revertOf;
}
+ public PatchSet.Id getCherryPickOf() {
+ return cherryPickOf;
+ }
+
+ public void setCherryPickOf(@Nullable PatchSet.Id cherryPickOf) {
+ this.cherryPickOf = cherryPickOf;
+ }
+
@Override
public String toString() {
return new StringBuilder(getClass().getSimpleName())
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index 8b93dbc..4a33bd7 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -124,13 +124,17 @@
return id();
}
+ public String getCommaSeparatedChangeAndPatchSetId() {
+ return changeId().toString() + ',' + id();
+ }
+
public String toRefName() {
return changeId().refPrefixBuilder().append(id()).toString();
}
@Override
public final String toString() {
- return changeId().toString() + ',' + id();
+ return getCommaSeparatedChangeAndPatchSetId();
}
}
diff --git a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
index 5b066ea..25e68f9 100644
--- a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
@@ -29,6 +29,8 @@
private final ProtoConverter<Entities.Change_Id, Change.Id> changeIdConverter =
ChangeIdProtoConverter.INSTANCE;
+ private final ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
+ PatchSetIdProtoConverter.INSTANCE;
private final ProtoConverter<Entities.Change_Key, Change.Key> changeKeyConverter =
ChangeKeyProtoConverter.INSTANCE;
private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
@@ -78,6 +80,10 @@
if (revertOf != null) {
builder.setRevertOf(changeIdConverter.toProto(revertOf));
}
+ PatchSet.Id cherryPickOf = change.getCherryPickOf();
+ if (cherryPickOf != null) {
+ builder.setCherryPickOf(patchSetIdConverter.toProto(cherryPickOf));
+ }
return builder.build();
}
@@ -118,6 +124,9 @@
if (proto.hasRevertOf()) {
change.setRevertOf(changeIdConverter.fromProto(proto.getRevertOf()));
}
+ if (proto.hasCherryPickOf()) {
+ change.setCherryPickOf(patchSetIdConverter.fromProto(proto.getCherryPickOf()));
+ }
return change;
}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 0d7c5c7..3b3f2ad 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -53,6 +53,8 @@
public Boolean hasReviewStarted;
public Integer revertOf;
public String submissionId;
+ public Integer cherryPickOfChange;
+ public Integer cherryPickOfPatchSet;
public int _number;
diff --git a/java/com/google/gerrit/server/change/ActionJson.java b/java/com/google/gerrit/server/change/ActionJson.java
index 031c1f2..e87cf70 100644
--- a/java/com/google/gerrit/server/change/ActionJson.java
+++ b/java/com/google/gerrit/server/change/ActionJson.java
@@ -144,6 +144,8 @@
copy.unresolvedCommentCount = changeInfo.unresolvedCommentCount;
copy.workInProgress = changeInfo.workInProgress;
copy.id = changeInfo.id;
+ copy.cherryPickOfChange = changeInfo.cherryPickOfChange;
+ copy.cherryPickOfPatchSet = changeInfo.cherryPickOfPatchSet;
return copy;
}
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index d1c27d5..4980c4a 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -111,6 +111,7 @@
private final String refName;
// Fields exposed as setters.
+ private PatchSet.Id cherryPickOf;
private Change.Status status;
private String topic;
private String message;
@@ -189,6 +190,7 @@
ctx.getWhen());
change.setStatus(MoreObjects.firstNonNull(status, Change.Status.NEW));
change.setTopic(topic);
+ change.setCherryPickOf(cherryPickOf);
change.setPrivate(isPrivate);
change.setWorkInProgress(workInProgress);
change.setReviewStarted(!workInProgress);
@@ -227,6 +229,11 @@
return this;
}
+ public ChangeInserter setCherryPickOf(PatchSet.Id cherryPickOf) {
+ this.cherryPickOf = cherryPickOf;
+ return this;
+ }
+
public ChangeInserter setMessage(String message) {
this.message = message;
return this;
@@ -378,6 +385,9 @@
if (revertOf != null) {
update.setRevertOf(revertOf.get());
}
+ if (cherryPickOf != null) {
+ update.setCherryPickOf(cherryPickOf.getCommaSeparatedChangeAndPatchSetId());
+ }
List<String> newGroups = groups;
if (newGroups.isEmpty()) {
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 21ee28a..974cb47 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -588,6 +588,12 @@
}
out.revertOf = cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null;
out.submissionId = cd.change().getSubmissionId();
+ out.cherryPickOfChange =
+ cd.change().getCherryPickOf() != null
+ ? cd.change().getCherryPickOf().changeId().get()
+ : null;
+ out.cherryPickOfPatchSet =
+ cd.change().getCherryPickOf() != null ? cd.change().getCherryPickOf().get() : null;
if (has(REVIEWER_UPDATES)) {
out.reviewerUpdates = reviewerUpdates(cd);
diff --git a/java/com/google/gerrit/server/change/SetCherryPickOp.java b/java/com/google/gerrit/server/change/SetCherryPickOp.java
new file mode 100644
index 0000000..d271923
--- /dev/null
+++ b/java/com/google/gerrit/server/change/SetCherryPickOp.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2019 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.server.change;
+
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class SetCherryPickOp implements BatchUpdateOp {
+ public interface Factory {
+ SetCherryPickOp create(PatchSet.Id cherryPickOf);
+ }
+
+ private final PatchSet.Id newCherryPickOf;
+
+ @Inject
+ SetCherryPickOp(@Assisted PatchSet.Id newCherryPickOf) {
+ this.newCherryPickOf = newCherryPickOf;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws RestApiException {
+ Change change = ctx.getChange();
+ if (newCherryPickOf.equals(change.getCherryPickOf())) {
+ return false;
+ }
+
+ ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
+ update.setCherryPickOf(newCherryPickOf.getCommaSeparatedChangeAndPatchSetId());
+ return true;
+ }
+}
diff --git a/java/com/google/gerrit/server/data/ChangeAttribute.java b/java/com/google/gerrit/server/data/ChangeAttribute.java
index a6da2b9..bc51e2a 100644
--- a/java/com/google/gerrit/server/data/ChangeAttribute.java
+++ b/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -31,6 +31,8 @@
public String url;
public String commitMessage;
public List<String> hashtags;
+ public Integer cherryPickOfChange;
+ public Integer cherryPickOfPatchSet;
public Long createdOn;
public Long lastUpdated;
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 3f22d7f..9422c18 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -144,6 +144,10 @@
a.createdOn = change.getCreatedOn().getTime() / 1000L;
a.wip = change.isWorkInProgress() ? true : null;
a.isPrivate = change.isPrivate() ? true : null;
+ a.cherryPickOfChange =
+ change.getCherryPickOf() != null ? change.getCherryPickOf().changeId().get() : null;
+ a.cherryPickOfPatchSet =
+ change.getCherryPickOf() != null ? change.getCherryPickOf().get() : null;
return a;
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 07e9b9e4..d3a0065 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -269,6 +269,24 @@
public static final FieldDef<ChangeData, Integer> OWNER =
integer(ChangeQueryBuilder.FIELD_OWNER).build(changeGetter(c -> c.getOwner().get()));
+ /** References the source change number that this change was cherry-picked from. */
+ public static final FieldDef<ChangeData, Integer> CHERRY_PICK_OF_CHANGE =
+ integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_CHANGE)
+ .build(
+ cd ->
+ cd.change().getCherryPickOf() != null
+ ? cd.change().getCherryPickOf().changeId().get()
+ : null);
+
+ /** References the source change patch-set that this change was cherry-picked from. */
+ public static final FieldDef<ChangeData, Integer> CHERRY_PICK_OF_PATCHSET =
+ integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_PATCHSET)
+ .build(
+ cd ->
+ cd.change().getCherryPickOf() != null
+ ? cd.change().getCherryPickOf().get()
+ : null);
+
/** The user assigned to the change. */
public static final FieldDef<ChangeData, Integer> ASSIGNEE =
integer(ChangeQueryBuilder.FIELD_ASSIGNEE)
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 55fe11d..6d6d4b8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -91,6 +91,7 @@
// New numeric types: use dimensional points using the k-d tree geo-spatial data structure
// to offer fast single- and multi-dimensional numeric range. As the consequense, integer
// document id type is replaced with string document id type.
+ @Deprecated
static final Schema<ChangeData> V57 =
new Schema.Builder<ChangeData>()
.add(V56)
@@ -99,6 +100,14 @@
.legacyNumericFields(false)
.build();
+ // Add new field CHERRY_PICK_OF
+ static final Schema<ChangeData> V58 =
+ new Schema.Builder<ChangeData>()
+ .add(V57)
+ .add(ChangeField.CHERRY_PICK_OF_CHANGE)
+ .add(ChangeField.CHERRY_PICK_OF_PATCHSET)
+ .build();
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index b221ef5..cebb67d 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -48,6 +48,7 @@
public static final FooterKey FOOTER_TAG = new FooterKey("Tag");
public static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress");
public static final FooterKey FOOTER_REVERT_OF = new FooterKey("Revert-of");
+ public static final FooterKey FOOTER_CHERRY_PICK_OF = new FooterKey("Cherry-pick-of");
static final String AUTHOR = "Author";
static final String BASE_PATCH_SET = "Base-for-patch-set";
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 3322b68..369d1f2 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHERRY_PICK_OF;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
@@ -148,6 +149,7 @@
private ReviewerByEmailSet pendingReviewersByEmail;
private Change.Id revertOf;
private int updateCount;
+ private PatchSet.Id cherryPickOf;
ChangeNotesParser(
Change.Id changeId,
@@ -246,6 +248,7 @@
firstNonNull(workInProgress, false),
firstNonNull(hasReviewStarted, true),
revertOf,
+ cherryPickOf,
updateCount);
}
@@ -417,6 +420,10 @@
revertOf = parseRevertOf(commit);
}
+ if (cherryPickOf == null) {
+ cherryPickOf = parseCherryPickOf(commit);
+ }
+
previousWorkInProgressFooter = null;
parseWorkInProgress(commit);
}
@@ -966,6 +973,18 @@
return Change.id(revertOf);
}
+ private PatchSet.Id parseCherryPickOf(ChangeNotesCommit commit) throws ConfigInvalidException {
+ String cherryPickOf = parseOneFooter(commit, FOOTER_CHERRY_PICK_OF);
+ if (cherryPickOf == null) {
+ return null;
+ }
+ try {
+ return PatchSet.Id.parse(cherryPickOf);
+ } catch (IllegalArgumentException e) {
+ throw new ConfigInvalidException("\"" + cherryPickOf + "\" is not a valid patchset", e);
+ }
+ }
+
private void pruneReviewers() {
Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Timestamp>> rit =
reviewers.cellSet().iterator();
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 896cca3..064e43b 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -123,6 +123,7 @@
boolean workInProgress,
boolean reviewStarted,
@Nullable Change.Id revertOf,
+ @Nullable PatchSet.Id cherryPickOf,
int updateCount) {
requireNonNull(
metaId,
@@ -152,6 +153,7 @@
.workInProgress(workInProgress)
.reviewStarted(reviewStarted)
.revertOf(revertOf)
+ .cherryPickOf(cherryPickOf)
.build())
.hashtags(hashtags)
.serverId(serverId)
@@ -220,6 +222,9 @@
@Nullable
abstract Change.Id revertOf();
+ @Nullable
+ abstract PatchSet.Id cherryPickOf();
+
abstract Builder toBuilder();
@AutoValue.Builder
@@ -254,6 +259,8 @@
abstract Builder revertOf(@Nullable Change.Id revertOf);
+ abstract Builder cherryPickOf(@Nullable PatchSet.Id cherryPickOf);
+
abstract ChangeColumns build();
}
}
@@ -341,6 +348,7 @@
change.setWorkInProgress(c.workInProgress());
change.setReviewStarted(c.reviewStarted());
change.setRevertOf(c.revertOf());
+ change.setCherryPickOf(c.cherryPickOf());
if (!patchSets().isEmpty()) {
change.setCurrentPatchSet(c.currentPatchSetId(), c.subject(), c.originalSubject());
@@ -514,6 +522,10 @@
if (cols.revertOf() != null) {
b.setRevertOf(cols.revertOf().get()).setHasRevertOf(true);
}
+ if (cols.cherryPickOf() != null) {
+ b.setCherryPickOf(cols.cherryPickOf().getCommaSeparatedChangeAndPatchSetId())
+ .setHasCherryPickOf(true);
+ }
return b.build();
}
@@ -637,6 +649,9 @@
if (proto.getHasRevertOf()) {
b.revertOf(Change.id(proto.getRevertOf()));
}
+ if (proto.getHasCherryPickOf()) {
+ b.cherryPickOf(PatchSet.Id.parse(proto.getCherryPickOf()));
+ }
return b.build();
}
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 6a900c0..2822f61 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -21,6 +21,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_ASSIGNEE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_BRANCH;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHANGE_ID;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CHERRY_PICK_OF;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_COMMIT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_CURRENT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_GROUPS;
@@ -137,6 +138,7 @@
private Boolean isPrivate;
private Boolean workInProgress;
private Integer revertOf;
+ private String cherryPickOf;
private ChangeDraftUpdate draftUpdate;
private RobotCommentUpdate robotCommentUpdate;
@@ -417,6 +419,10 @@
rootOnly = true;
}
+ public void setCherryPickOf(String cherryPickOf) {
+ this.cherryPickOf = cherryPickOf;
+ }
+
/** @return the tree id for the updated tree */
private ObjectId storeRevisionNotes(RevWalk rw, ObjectInserter inserter, ObjectId curr)
throws ConfigInvalidException, IOException {
@@ -665,6 +671,10 @@
addFooter(msg, FOOTER_REVERT_OF, revertOf);
}
+ if (cherryPickOf != null) {
+ addFooter(msg, FOOTER_CHERRY_PICK_OF, cherryPickOf);
+ }
+
cb.setMessage(msg.toString());
try {
ObjectId treeId = storeRevisionNotes(rw, ins, curr);
@@ -714,7 +724,8 @@
&& !currentPatchSet
&& isPrivate == null
&& workInProgress == null
- && revertOf == null;
+ && revertOf == null
+ && cherryPickOf == null;
}
ChangeDraftUpdate getDraftUpdate() {
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 78ca0fc..381dc6e 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -301,6 +301,7 @@
private Integer unresolvedCommentCount;
private Integer totalCommentCount;
private LabelTypes labelTypes;
+ private Optional<PatchSet.Id> cherryPickOf;
private ImmutableList<byte[]> refStates;
private ImmutableList<byte[]> refStatePatterns;
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 5f076b1..7747998 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -34,6 +34,7 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.NotSignedInException;
import com.google.gerrit.exceptions.StorageException;
@@ -189,6 +190,9 @@
public static final String FIELD_WATCHEDBY = "watchedby";
public static final String FIELD_WIP = "wip";
public static final String FIELD_REVERTOF = "revertof";
+ public static final String FIELD_CHERRY_PICK_OF = "cherrypickof";
+ public static final String FIELD_CHERRY_PICK_OF_CHANGE = "cherrypickofchange";
+ public static final String FIELD_CHERRY_PICK_OF_PATCHSET = "cherrypickofpatchset";
public static final String ARG_ID_USER = "user";
public static final String ARG_ID_GROUP = "group";
@@ -1268,6 +1272,29 @@
"'submissionid' operator is not supported by change index version");
}
+ @Operator
+ public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_CHANGE)
+ && args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_PATCHSET)) {
+ if (Ints.tryParse(value) != null) {
+ return new CherryPickOfChangePredicate(value);
+ }
+ try {
+ PatchSet.Id patchSetId = PatchSet.Id.parse(value);
+ return Predicate.and(
+ new CherryPickOfChangePredicate(patchSetId.changeId().toString()),
+ new CherryPickOfPatchSetPredicate(patchSetId.getId()));
+ } catch (IllegalArgumentException e) {
+ throw new QueryParseException(
+ "'"
+ + value
+ + "' is not a valid input. It must be in the 'ChangeNumber[,PatchsetNumber]' format.");
+ }
+ }
+ throw new QueryParseException(
+ "'cherrypickof' operator is not supported by change index version");
+ }
+
@Override
protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
if (query.startsWith("refs/")) {
diff --git a/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java b/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java
new file mode 100644
index 0000000..d452017
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2019 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.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class CherryPickOfChangePredicate extends ChangeIndexPredicate {
+ public CherryPickOfChangePredicate(String cherryPickOfChange) {
+ super(ChangeField.CHERRY_PICK_OF_CHANGE, cherryPickOfChange);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) {
+ if (cd.change().getCherryPickOf() == null) {
+ return false;
+ }
+ return Integer.toString(cd.change().getCherryPickOf().changeId().get()).equals(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java b/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java
new file mode 100644
index 0000000..888f45d
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2019 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.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+
+public class CherryPickOfPatchSetPredicate extends ChangeIndexPredicate {
+ public CherryPickOfPatchSetPredicate(String cherryPickOfPatchSet) {
+ super(ChangeField.CHERRY_PICK_OF_PATCHSET, cherryPickOfPatchSet);
+ }
+
+ @Override
+ public boolean match(ChangeData cd) {
+ if (cd.change().getCherryPickOf() == null) {
+ return false;
+ }
+ return cd.change().getCherryPickOf().getId().equals(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index a7a7998..255c15c 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -42,6 +42,7 @@
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.change.SetCherryPickOp;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -102,6 +103,7 @@
private final Provider<IdentifiedUser> user;
private final ChangeInserter.Factory changeInserterFactory;
private final PatchSetInserter.Factory patchSetInserterFactory;
+ private final SetCherryPickOp.Factory setCherryPickOfFactory;
private final MergeUtil.Factory mergeUtilFactory;
private final ChangeNotes.Factory changeNotesFactory;
private final ProjectCache projectCache;
@@ -117,6 +119,7 @@
Provider<IdentifiedUser> user,
ChangeInserter.Factory changeInserterFactory,
PatchSetInserter.Factory patchSetInserterFactory,
+ SetCherryPickOp.Factory setCherryPickOfFactory,
MergeUtil.Factory mergeUtilFactory,
ChangeNotes.Factory changeNotesFactory,
ProjectCache projectCache,
@@ -129,6 +132,7 @@
this.user = user;
this.changeInserterFactory = changeInserterFactory;
this.patchSetInserterFactory = patchSetInserterFactory;
+ this.setCherryPickOfFactory = setCherryPickOfFactory;
this.mergeUtilFactory = mergeUtilFactory;
this.changeNotesFactory = changeNotesFactory;
this.projectCache = projectCache;
@@ -374,7 +378,13 @@
dest.project(),
destChanges.get(0).getId().get()));
}
- changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit);
+ changeId =
+ insertPatchSet(
+ bu,
+ git,
+ destChanges.get(0).notes(),
+ cherryPickCommit,
+ sourceChange.currentPatchSetId());
} else {
// Change key not found on destination branch. We can create a new
// change.
@@ -457,13 +467,22 @@
}
private Change.Id insertPatchSet(
- BatchUpdate bu, Repository git, ChangeNotes destNotes, CodeReviewCommit cherryPickCommit)
+ BatchUpdate bu,
+ Repository git,
+ ChangeNotes destNotes,
+ CodeReviewCommit cherryPickCommit,
+ PatchSet.Id sourcePatchSetId)
throws IOException {
Change destChange = destNotes.getChange();
PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
inserter.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".");
bu.addOp(destChange.getId(), inserter);
+ if (destChange.getCherryPickOf() == null
+ || !destChange.getCherryPickOf().equals(sourcePatchSetId)) {
+ SetCherryPickOp cherryPickOfUpdater = setCherryPickOfFactory.create(sourcePatchSetId);
+ bu.addOp(destChange.getId(), cherryPickOfUpdater);
+ }
return destChange.getId();
}
@@ -483,6 +502,7 @@
ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
ins.setRevertOf(revertOf);
BranchNameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
+ PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
ins.setMessage(
revertOf == null
? messageForDestinationChange(
@@ -490,6 +510,7 @@
: "Uploaded patch set 1.") // For revert commits, the message should not include
// cherry-pick information.
.setTopic(topic)
+ .setCherryPickOf(sourcePatchSetId)
.setWorkInProgress(
(sourceChange != null && sourceChange.isWorkInProgress())
|| !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index 4409c6a..e339c67 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -40,6 +40,7 @@
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.SetAssigneeOp;
+import com.google.gerrit.server.change.SetCherryPickOp;
import com.google.gerrit.server.change.SetHashtagsOp;
import com.google.gerrit.server.change.SetPrivateOp;
import com.google.gerrit.server.change.WorkInProgressOp;
@@ -201,6 +202,7 @@
factory(RebaseChangeOp.Factory.class);
factory(ReviewerResource.Factory.class);
factory(SetAssigneeOp.Factory.class);
+ factory(SetCherryPickOp.Factory.class);
factory(SetHashtagsOp.Factory.class);
factory(SetPrivateOp.Factory.class);
factory(WorkInProgressOp.Factory.class);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 61d0fd5..c4dec31 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -337,6 +337,9 @@
assertThat(cherry.get().subject).contains(in.message);
assertThat(cherry.get().topic).isEqualTo("someTopic-foo");
+ assertThat(cherry.get().cherryPickOfChange).isEqualTo(orig.get()._number);
+ assertThat(cherry.get().cherryPickOfPatchSet).isEqualTo(1);
+
cherry.current().review(ReviewInput.approve());
cherry.current().submit();
}
@@ -416,23 +419,23 @@
ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
+ assertThat(cherry.get().cherryPickOfChange).isEqualTo(orig.get()._number);
+ assertThat(cherry.get().cherryPickOfPatchSet).isEqualTo(1);
assertThat(cherry.get().workInProgress).isTrue();
}
@Test
public void cherryPickToSameBranch() throws Exception {
PushOneCommit.Result r = createChange();
+ ChangeApi change = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
CherryPickInput in = new CherryPickInput();
in.destination = "master";
in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
- ChangeInfo cherryInfo =
- gApi.changes()
- .id(project.get() + "~master~" + r.getChangeId())
- .revision(r.getCommit().name())
- .cherryPick(in)
- .get();
+ ChangeInfo cherryInfo = change.revision(r.getCommit().name()).cherryPick(in).get();
assertThat(cherryInfo.messages).hasSize(2);
Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
+ assertThat(cherryInfo.cherryPickOfChange).isEqualTo(change.get()._number);
+ assertThat(cherryInfo.cherryPickOfPatchSet).isEqualTo(1);
assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
}
@@ -625,6 +628,42 @@
}
@Test
+ public void cherryPickToExistingChangeUpdatesCherryPickOf() throws Exception {
+ PushOneCommit.Result r1 =
+ pushFactory
+ .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "a")
+ .to("refs/for/master");
+ String t1 = project.get() + "~master~" + r1.getChangeId();
+ ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r1.getChangeId());
+
+ BranchInput bin = new BranchInput();
+ bin.revision = r1.getCommit().getParent(0).name();
+ gApi.projects().name(project.get()).branch("foo").create(bin);
+
+ PushOneCommit.Result r2 =
+ pushFactory
+ .create(admin.newIdent(), testRepo, SUBJECT, FILE_NAME, "b", r1.getChangeId())
+ .to("refs/for/foo");
+ String t2 = project.get() + "~foo~" + r2.getChangeId();
+
+ CherryPickInput in = new CherryPickInput();
+ in.destination = "foo";
+ in.message = r1.getCommit().getFullMessage();
+ ChangeApi cherry = gApi.changes().id(t1).current().cherryPick(in);
+ assertThat(get(t2, ALL_REVISIONS).revisions).hasSize(2);
+ assertThat(cherry.get().cherryPickOfChange).isEqualTo(orig.get()._number);
+ assertThat(cherry.get().cherryPickOfPatchSet).isEqualTo(1);
+
+ PushOneCommit.Result r3 = amendChange(r1.getChangeId(), SUBJECT, "b.txt", "b");
+ in = new CherryPickInput();
+ in.destination = "foo";
+ in.message = r3.getCommit().getFullMessage();
+ cherry = gApi.changes().id(t1).current().cherryPick(in);
+ assertThat(cherry.get().cherryPickOfChange).isEqualTo(orig.get()._number);
+ assertThat(cherry.get().cherryPickOfPatchSet).isEqualTo(2);
+ }
+
+ @Test
public void cherryPickToExistingChange() throws Exception {
PushOneCommit.Result r1 =
pushFactory
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
index 72e4a7a..bc669cc 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
@@ -335,6 +335,7 @@
.put("workInProgress", boolean.class)
.put("reviewStarted", boolean.class)
.put("revertOf", Change.Id.class)
+ .put("cherryPickOf", PatchSet.Id.class)
.build());
}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index 6ece894..16981dc 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -776,6 +776,7 @@
.put("workInProgress", boolean.class)
.put("reviewStarted", boolean.class)
.put("revertOf", Change.Id.class)
+ .put("cherryPickOf", PatchSet.Id.class)
.put("toBuilder", ChangeNotesState.ChangeColumns.Builder.class)
.build());
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 539b1b6..9073342 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -270,6 +270,19 @@
</template>
</span>
</section>
+ <template is="dom-if" if="[[_showCherryPickOf(change.*)]]">
+ <section>
+ <span class="title">Cherry pick of</span>
+ <span class="value">
+ <a href$="[[_computeCherryPickOfURL(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]">
+ <gr-limited-text
+ text="[[change.cherry_pick_of_change]],[[change.cherry_pick_of_patch_set]]"
+ limit="40">
+ </gr-limited-text>
+ </a>
+ </span>
+ </section>
+ </template>
<section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden>
<span class="title">Strategy</span>
<span class="value">[[_computeStrategy(change)]]</span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 94519b0..2eab284 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -227,6 +227,13 @@
return hasTopic && !settingTopic;
}
+ _showCherryPickOf(changeRecord) {
+ const hasCherryPickOf = !!changeRecord &&
+ !!changeRecord.base && !!changeRecord.base.cherry_pick_of_change &&
+ !!changeRecord.base.cherry_pick_of_patch_set;
+ return hasCherryPickOf;
+ }
+
_handleHashtagChanged(e) {
const lastHashtag = this.change.hashtag;
if (!this._newHashtag.length) { return; }
@@ -353,6 +360,10 @@
this.change.status.toLowerCase());
}
+ _computeCherryPickOfURL(change, patchset, project) {
+ return Gerrit.Nav.getUrlForChangeById(change, project, patchset);
+ }
+
_computeTopicURL(topic) {
return Gerrit.Nav.getUrlForTopic(topic);
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 148d917..09cb4cc 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -464,6 +464,22 @@
assert.isTrue(element._showTopicChip({base: {topic: 'foo'}}, false));
});
+ test('_showCherryPickOf', () => {
+ assert.isFalse(element._showCherryPickOf(null));
+ assert.isFalse(element._showCherryPickOf({
+ base: {
+ cherry_pick_of_change: null,
+ cherry_pick_of_patch_set: null,
+ },
+ }));
+ assert.isTrue(element._showCherryPickOf({
+ base: {
+ cherry_pick_of_change: 123,
+ cherry_pick_of_patch_set: 1,
+ },
+ }));
+ });
+
suite('Topic removal', () => {
let change;
setup(() => {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 48e8662..ee44e06 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -29,6 +29,7 @@
'cc:',
'cc:self',
'change:',
+ 'cherrypickof:',
'comment:',
'commentby:',
'commit:',
diff --git a/proto/cache.proto b/proto/cache.proto
index 10e0216..8f73388 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -84,7 +84,7 @@
int32 change_id = 2;
- // Next ID: 24
+ // Next ID: 26
message ChangeColumnsProto {
string change_key = 1;
@@ -124,6 +124,9 @@
int32 revert_of = 22;
bool has_revert_of = 23;
+
+ string cherry_pick_of = 24;
+ bool has_cherry_pick_of = 25;
}
// Effectively required, even though the corresponding ChangeNotesState field
// is optional, since the field is only absent when NoteDb is disabled, in
diff --git a/proto/entities.proto b/proto/entities.proto
index 374b47c..84c7fbd 100644
--- a/proto/entities.proto
+++ b/proto/entities.proto
@@ -31,7 +31,7 @@
}
// Serialized form of com.google.gerrit.entities.Change.
-// Next ID: 24
+// Next ID: 25
message Change {
required Change_Id change_id = 1;
optional Change_Key change_key = 2;
@@ -51,6 +51,7 @@
optional bool work_in_progress = 21;
optional bool review_started = 22;
optional Change_Id revert_of = 23;
+ optional PatchSet_Id cherry_pick_of = 24;
// Deleted fields, should not be reused:
reserved 6; // sortkey