Skip to content

Commit 1bcecc5

Browse files
committed
Reduce extra merges by cherry-picking parallel builds to branch
Fixes: QTQAINFRA-4345 Change-Id: I81893d21c561aceac38d452c7382781be6cb7f8a Reviewed-by: Edward Welbourne <[email protected]>
1 parent 2e518c7 commit 1bcecc5

File tree

4 files changed

+300
-38
lines changed

4 files changed

+300
-38
lines changed

src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtCommandBuildApprove.java

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -183,17 +183,6 @@ private void runBuildApprove() throws UnloggedFailure {
183183
if (git.resolve(destBranchKey.branch()) == null) throw die("branch not found");
184184
if (git.resolve(buildBranchKey.branch()) == null) throw die("build not found");
185185

186-
// Initialize and populate open changes list.
187-
affectedChanges = qtUtil.listChangesNotMerged(git, buildBranchKey, destBranchKey);
188-
189-
// Notify user that build did not have any open changes. The build has already been approved.
190-
if (affectedChanges.isEmpty()) {
191-
logger.atInfo().log(
192-
"qtcodereview: staging-approve build %s already in project %s branch %s",
193-
buildBranch, projectKey, destBranchKey);
194-
throw die("No open changes in the build branch");
195-
}
196-
197186
if (result.toLowerCase().equals(PASS)) {
198187
approveBuildChanges();
199188
} else if (result.toLowerCase().equals(FAIL)) {
@@ -212,8 +201,6 @@ private void runBuildApprove() throws UnloggedFailure {
212201
throw die(e.getMessage());
213202
} catch (QtUtil.BranchNotFoundException e) {
214203
throw die("invalid branch " + e.getMessage());
215-
} catch (NoSuchRefException e) {
216-
throw die("invalid reference " + e.getMessage());
217204
} catch (UpdateException | RestApiException | ConfigInvalidException e) {
218205
logger.atSevere().log("qtcodereview: staging-napprove failed to update change status %s", e);
219206
throw die("Failed to update change status");
@@ -228,20 +215,25 @@ private void runBuildApprove() throws UnloggedFailure {
228215
}
229216

230217
private void approveBuildChanges()
231-
throws QtUtil.MergeConflictException, NoSuchRefException, IOException, UpdateException,
232-
RestApiException, ConfigInvalidException {
218+
throws QtUtil.MergeConflictException, IOException, UpdateException, UnloggedFailure,
219+
RestApiException, ConfigInvalidException, QtUtil.BranchNotFoundException {
233220
if (message == null) message = String.format("Change merged into branch %s", destBranchKey);
234221

235222
ObjectId oldId = git.resolve(destBranchKey.branch());
236223

237-
Result result = QtUtil.mergeBranches(user.asIdentifiedUser(), git, buildBranchKey,
238-
destBranchKey, "Merge integration " + buildBranch);
239-
240-
if (result != Result.FAST_FORWARD) {
224+
try {
225+
affectedChanges = qtUtil.mergeIntegrationToBranch(user.asIdentifiedUser(), git, projectKey,
226+
buildBranchKey, destBranchKey, "Merge integration " + buildBranch);
227+
} catch (NoSuchRefException e) {
228+
message = "Gerrit plugin internal error. Please contact Gerrit Admin.";
229+
logger.atInfo().log(e.getMessage());
230+
rejectBuildChanges();
231+
return;
232+
} catch (QtUtil.MergeConflictException e) {
241233
message = "Unable to merge this integration because another integration parallel to this one "
242234
+ "successfully merged first and created a conflict in one of the tested changes.\n"
243235
+ "Please review, resolve conflicts if necessary, and restage.";
244-
logger.atInfo().log(message);
236+
logger.atInfo().log(e.getMessage());
245237
rejectBuildChanges();
246238
return;
247239
}
@@ -266,9 +258,19 @@ private void approveBuildChanges()
266258

267259
private void rejectBuildChanges()
268260
throws QtUtil.MergeConflictException, UpdateException, RestApiException, IOException,
269-
ConfigInvalidException {
261+
ConfigInvalidException, QtUtil.BranchNotFoundException, UnloggedFailure {
270262
if (message == null) message = String.format("Change rejected for branch %s", destBranchKey);
271263

264+
affectedChanges = qtUtil.listChangesNotMerged(git, buildBranchKey, destBranchKey);
265+
266+
// Notify user that build did not have any open changes. The build has already been approved.
267+
if (affectedChanges.isEmpty()) {
268+
logger.atInfo().log(
269+
"staging-approve build %s already in project %s branch %s",
270+
buildBranch, projectKey, destBranchKey);
271+
throw die("No open changes in the build branch");
272+
}
273+
272274
updateChanges(
273275
affectedChanges,
274276
Change.Status.NEW,
@@ -360,9 +362,6 @@ private void sendMergeEvent(ChangeData changeData) {
360362
PatchSet ps = changeData.currentPatchSet();
361363
changeMerged.fire(
362364
changeData.change(), ps, user.asIdentifiedUser().state(), ps.commitId().name(), ts);
363-
364-
// logger.atInfo().log("qtcodereview: staging-approve sending merge event failed for %s",
365-
// changeData.change());
366365
}
367366

368367
private void readMessageParameter() throws UnloggedFailure {

src/main/java/com/googlesource/gerrit/plugins/qtcodereview/QtUtil.java

Lines changed: 221 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@
3434
import com.google.gerrit.server.events.ChangeEvent;
3535
import com.google.gerrit.server.events.EventDispatcher;
3636
import com.google.gerrit.server.events.EventFactory;
37+
import com.google.gerrit.server.git.CodeReviewCommit;
38+
import com.google.gerrit.server.git.MergeUtil;
3739
import com.google.gerrit.server.notedb.ChangeNotes;
3840
import com.google.gerrit.server.permissions.PermissionBackendException;
3941
import com.google.gerrit.server.project.NoSuchRefException;
42+
import com.google.gerrit.server.project.ProjectCache;
43+
import com.google.gerrit.server.project.ProjectState;
4044
import com.google.gerrit.server.query.change.ChangeData;
4145
import com.google.gerrit.server.query.change.InternalChangeQuery;
4246
import com.google.gerrit.server.submit.IntegrationException;
@@ -50,6 +54,7 @@
5054
import java.sql.Timestamp;
5155
import java.util.AbstractMap;
5256
import java.util.ArrayList;
57+
import java.util.Date;
5358
import java.util.HashMap;
5459
import java.util.Iterator;
5560
import java.util.List;
@@ -60,6 +65,7 @@
6065
import org.eclipse.jgit.lib.Constants;
6166
import org.eclipse.jgit.lib.ObjectId;
6267
import org.eclipse.jgit.lib.ObjectInserter;
68+
import org.eclipse.jgit.lib.ObjectReader;
6369
import org.eclipse.jgit.lib.PersonIdent;
6470
import org.eclipse.jgit.lib.Ref;
6571
import org.eclipse.jgit.lib.RefUpdate;
@@ -92,6 +98,8 @@ public class QtUtil {
9298
private final ChangeNotes.Factory changeNotesFactory;
9399
private final DynamicItem<EventDispatcher> eventDispatcher;
94100
private final EventFactory eventFactory;
101+
private final MergeUtil.Factory mergeUtilFactory;
102+
private final ProjectCache projectCache;
95103
private final QtCherryPickPatch qtCherryPickPatch;
96104
private final QtChangeUpdateOp.Factory qtUpdateFactory;
97105
private final QtEmailSender qtEmailSender;
@@ -104,6 +112,8 @@ public class QtUtil {
104112
ChangeNotes.Factory changeNotesFactory,
105113
EventFactory eventFactory,
106114
DynamicItem<EventDispatcher> eventDispatcher,
115+
MergeUtil.Factory mergeUtilFactory,
116+
ProjectCache projectCache,
107117
QtCherryPickPatch qtCherryPickPatch,
108118
QtChangeUpdateOp.Factory qtUpdateFactory,
109119
QtEmailSender qtEmailSender) {
@@ -113,6 +123,8 @@ public class QtUtil {
113123
this.changeNotesFactory = changeNotesFactory;
114124
this.eventDispatcher = eventDispatcher;
115125
this.eventFactory = eventFactory;
126+
this.mergeUtilFactory = mergeUtilFactory;
127+
this.projectCache = projectCache;
116128
this.qtCherryPickPatch = qtCherryPickPatch;
117129
this.qtUpdateFactory = qtUpdateFactory;
118130
this.qtEmailSender = qtEmailSender;
@@ -604,11 +616,10 @@ public static RevCommit merge(
604616
String message = customCommitMessage;
605617
if (message == null) {
606618
try {
607-
message = revWalk.parseCommit(toMerge).getShortMessage();
619+
message = "Merge \"" + revWalk.parseCommit(toMerge).getShortMessage() + "\"";
608620
} catch (Exception e) {
609-
message = toMerge.toString();
621+
message = "Merge";
610622
}
611-
message = "Merge \"" + toMerge.toString() + "\"";
612623
}
613624

614625
final CommitBuilder mergeCommit = new CommitBuilder();
@@ -675,6 +686,213 @@ private static RefUpdate.Result mergeObjectToBranch(
675686
}
676687
}
677688

689+
private RefUpdate.Result fastForwardBranch(Repository git,
690+
String branchName, ObjectId toObjectId) {
691+
692+
RefUpdate.Result result = null;
693+
694+
try {
695+
RefUpdate refUpdate = git.updateRef(branchName);
696+
refUpdate.setNewObjectId(toObjectId);
697+
refUpdate.setForceUpdate(false);
698+
result = refUpdate.update();
699+
logger.atInfo().log("fastforward branch %s to %s, result: %s", branchName,
700+
toObjectId.name(), result);
701+
} catch (Exception e) {
702+
result = null;
703+
logger.atWarning().log("fastforward failed for %s: %s", branchName, e);
704+
}
705+
706+
return result;
707+
}
708+
709+
private List<RevCommit> listCommitsInIntegrationBranch(Repository git,
710+
ObjectId integrationHeadId, ObjectId targetBranchHeadId) {
711+
712+
List<RevCommit> commits = new ArrayList<RevCommit>();
713+
int count = 0;
714+
715+
try {
716+
RevWalk revWalk = new RevWalk(git);
717+
RevCommit commit = revWalk.parseCommit(integrationHeadId);
718+
RevCommit targetHeadCommit = revWalk.parseCommit(targetBranchHeadId);
719+
720+
do {
721+
if (revWalk.isMergedInto(commit, targetHeadCommit)) {
722+
commit = null;
723+
} else {
724+
commits.add(0, commit);
725+
if (commit.getParentCount() > 0) {
726+
// Qt Gerrit plugins's merges always have the branch as parent 0
727+
commit = revWalk.parseCommit(commit.getParent(0));
728+
} else commit = null;
729+
}
730+
count++;
731+
} while (commit != null && count < 100);
732+
} catch (Exception e) {
733+
commits = null;
734+
logger.atWarning().log("listing commits in a branch failed: %s", e);
735+
}
736+
737+
if (count >= 100) {
738+
logger.atWarning().log("listing commits in a branch failed: too many commmits");
739+
return null;
740+
}
741+
742+
return commits;
743+
}
744+
745+
private Boolean isCherryPickingAllowed(List<RevCommit> commits) {
746+
// Cherry-picking merge commits is not allowed, because it would squash
747+
// the merge into one single commit
748+
for (RevCommit commit : commits) {
749+
if (commit.getParentCount() > 1) return false;
750+
}
751+
return true;
752+
}
753+
754+
private List<RevCommit> cherryPickCommitsToBranch(Repository git,
755+
Project.NameKey project, String branchName, List<RevCommit> commits) {
756+
757+
try {
758+
ProjectState projectState = projectCache.checkedGet(project);
759+
MergeUtil mergeUtil = mergeUtilFactory.create(projectState, true);
760+
ObjectInserter objInserter = git.newObjectInserter();
761+
ObjectReader reader = objInserter.newReader();
762+
CodeReviewCommit.CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader);
763+
RevCommit newBranchHead = revWalk.parseCommit(git.resolve(branchName));
764+
765+
List<RevCommit> cherryPicked = new ArrayList<RevCommit>();
766+
767+
for (RevCommit commit : commits) {
768+
CodeReviewCommit cherryPickCommit =
769+
mergeUtil.createCherryPickFromCommit(
770+
objInserter,
771+
git.getConfig(),
772+
newBranchHead,
773+
commit,
774+
new PersonIdent(commit.getCommitterIdent(), new Date()),
775+
commit.getFullMessage(),
776+
revWalk,
777+
0,
778+
true, // ignoreIdenticalTree
779+
false); // allowConflicts
780+
objInserter.flush();
781+
logger.atInfo().log("created cherrypick commit %s from %s", cherryPickCommit.name(),
782+
commit.name());
783+
newBranchHead = cherryPickCommit;
784+
cherryPicked.add(cherryPickCommit);
785+
}
786+
787+
RefUpdate.Result result = fastForwardBranch(git, branchName, newBranchHead);
788+
if (result != RefUpdate.Result.FAST_FORWARD) return null;
789+
790+
return cherryPicked;
791+
} catch (Exception e) {
792+
logger.atWarning().log("cherrypicking commits to branch failed: %s", e);
793+
return null;
794+
}
795+
}
796+
797+
private List<Map.Entry<ChangeData, RevCommit>> listChanges(Repository git,
798+
BranchNameKey destination, List<RevCommit> commits) throws Exception {
799+
800+
Map<Change.Id, Map.Entry<ChangeData, RevCommit>> map = new HashMap<>();
801+
802+
for (RevCommit commit : commits) {
803+
String changeId = getChangeId(commit);
804+
List<ChangeData> changes = null;
805+
806+
if (changeId == null && commit.getParentCount() > 1) {
807+
// Merge commit without Change-Id, so done by plugin: changes merged in will be parent 1
808+
changeId = getChangeId(commit.getParent(1));
809+
}
810+
811+
if (changeId != null) {
812+
Change.Key key = Change.key(changeId);
813+
changes = queryProvider.get().byBranchKey(destination, key);
814+
}
815+
816+
if (changes != null && !changes.isEmpty()) {
817+
if (changes.size() > 1) {
818+
String msg = String.format("Same Change-Id in several changes on same branch: %s",
819+
commit.name());
820+
throw new Exception(msg);
821+
}
822+
ChangeData cd = changes.get(0);
823+
map.put(cd.getId(), new AbstractMap.SimpleEntry<ChangeData, RevCommit>(cd, commit));
824+
}
825+
}
826+
return new ArrayList<Map.Entry<ChangeData, RevCommit>>(map.values());
827+
}
828+
829+
public List<Map.Entry<ChangeData, RevCommit>> mergeIntegrationToBranch(
830+
IdentifiedUser user,
831+
Repository git,
832+
Project.NameKey project,
833+
final BranchNameKey integrationBranch,
834+
final BranchNameKey targetBranch,
835+
String customCommitMessage)
836+
throws MergeConflictException, NoSuchRefException {
837+
838+
List<RevCommit> commitsInBranch = null;
839+
RevCommit newBranchHead = null;
840+
RefUpdate.Result result = null;
841+
ObjectId sourceId = null;
842+
ObjectId targetId = null;
843+
844+
logger.atInfo().log("start merging integration %s to %s", integrationBranch.branch(),
845+
targetBranch.branch());
846+
847+
try {
848+
sourceId = git.resolve(integrationBranch.branch());
849+
if (sourceId == null) throw new NoSuchRefException("Invalid Revision: " + integrationBranch);
850+
851+
Ref targetRef = git.getRefDatabase().getRef(targetBranch.branch());
852+
if (targetRef == null) throw new NoSuchRefException("No such branch: " + targetBranch);
853+
854+
targetId = git.resolve(targetBranch.branch());
855+
if (targetId == null) throw new NoSuchRefException("Invalid Revision: " + targetBranch);
856+
857+
if (sourceId.equals(targetId)) throw new NoSuchRefException("Nothing to merge");
858+
859+
commitsInBranch = listCommitsInIntegrationBranch(git, sourceId, targetId);
860+
if (commitsInBranch == null)
861+
throw new NoSuchRefException("Failed to list commits in " + integrationBranch);
862+
else if (commitsInBranch.isEmpty())
863+
throw new NoSuchRefException("No commits in " + integrationBranch);
864+
} catch (Exception e) {
865+
logger.atWarning().log("preconditions of merging integration failed: %s", e);
866+
throw new NoSuchRefException(e.getMessage());
867+
}
868+
869+
try {
870+
logger.atInfo().log("Trying to fast forward...");
871+
result = fastForwardBranch(git, targetBranch.branch(), sourceId);
872+
if (result == RefUpdate.Result.FAST_FORWARD)
873+
return listChanges(git, targetBranch, commitsInBranch);
874+
875+
if (isCherryPickingAllowed(commitsInBranch)) {
876+
logger.atInfo().log("Trying to rebase integration onto the target branch...");
877+
List<RevCommit> cherrypicks = cherryPickCommitsToBranch(git, project, targetBranch.branch(), commitsInBranch);
878+
if (cherrypicks != null) return listChanges(git, targetBranch, cherrypicks);
879+
}
880+
881+
logger.atInfo().log("Trying to create merge commit...");
882+
List<Map.Entry<ChangeData, RevCommit>> mergedCommits =
883+
listChangesNotMerged(git, integrationBranch, targetBranch);
884+
result = mergeBranches(user, git, integrationBranch, targetBranch, customCommitMessage);
885+
if (result != RefUpdate.Result.FAST_FORWARD) throw new Exception("Merge conflict");
886+
return mergedCommits;
887+
} catch (Exception e) {
888+
result = null;
889+
logger.atWarning().log("Merging integration %s to %s failed: %s",
890+
integrationBranch.branch(), targetBranch.branch(), e.getMessage());
891+
throw new MergeConflictException("Merge conflict:" + integrationBranch.branch() +
892+
" to " + targetBranch.branch());
893+
}
894+
}
895+
678896
private Supplier<ChangeAttribute> changeAttributeSupplier(Change change, ChangeNotes notes) {
679897
return Suppliers.memoize(
680898
() -> {

src/test/java/com/googlesource/gerrit/plugins/qtcodereview/QtCodeReviewIT.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2019 The Qt Company
1+
// Copyright (C) 2021 The Qt Company
22

33
package com.googlesource.gerrit.plugins.qtcodereview;
44

@@ -380,4 +380,11 @@ protected RevCommit getRemoteHead(Project.NameKey project, String branch) throws
380380
protected RevCommit getRemoteHead() throws Exception {
381381
return getRemoteHead(project, "master");
382382
}
383+
384+
protected RevCommit loadCommit(RevCommit commit) throws Exception {
385+
Repository repo = repoManager.openRepository(project);
386+
RevWalk revWalk = new RevWalk(repo);
387+
return revWalk.parseCommit(commit);
388+
}
389+
383390
}

0 commit comments

Comments
 (0)