From f7d85ea91664a8baaccf00691a31d2b007b888e8 Mon Sep 17 00:00:00 2001 From: Zhen Chen Date: Mon, 2 May 2016 15:14:43 -0700 Subject: [PATCH] Add merge feature into /changes REST endpoint Allow user to create a merge in Gerrit via POST /changes REST API. By adding a "merge" attribute which could be a SHA1, a branch name or a tag name, etc. in the post JSON, gerrit will create a merge commit instead of an empty commit. If there are conflicts, the response will be rejected with a MergeConfictException which contains conflict message. Add dry run end point into GET /projects/{project}/branches/{branch}/mergeable with query parameters source (required) and strategy (optional) and return a MergeableInfo entity. Change-Id: I8f45f324704b3ff3eb20cb57c6e3bd75f2bf60ef --- Documentation/rest-api-changes.txt | 26 +++- Documentation/rest-api-projects.txt | 36 ++++++ .../gerrit/acceptance/PushOneCommit.java | 5 + .../rest/change/CreateChangeIT.java | 82 +++++++++++++ .../gerrit/extensions/common/ChangeInput.java | 1 + .../gerrit/extensions/common/MergeInput.java | 31 +++++ .../extensions/common/MergeableInfo.java | 2 + .../gerrit/server/change/CreateChange.java | 111 ++++++++++++------ .../gerrit/server/change/Mergeable.java | 2 + .../git/MergeIdenticalTreeException.java | 11 +- .../google/gerrit/server/git/MergeUtil.java | 68 ++++++++++- .../server/project/CheckMergeability.java | 107 +++++++++++++++++ .../google/gerrit/server/project/Module.java | 1 + 13 files changed, 441 insertions(+), 42 deletions(-) create mode 100644 gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index ca5d4606bf..9eb33fbf49 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt @@ -3057,7 +3057,8 @@ As response a link:#mergeable-info[MergeableInfo] entity is returned. )]}' { submit_type: "MERGE_IF_NECESSARY", - mergeable: true, + strategy: "recursive", + mergeable: true } ---- @@ -4248,6 +4249,8 @@ A link:#change-id[\{change-id\}] that identifies the base change for a create change operation. |`new_branch` |optional, default to `false`| Allow creating a new branch when set to `true`. +|`merge` |optional| +The detail of a merge commit as a link:#merge-input[MergeInput] entity. |================================== [[change-message-info]] @@ -4733,12 +4736,33 @@ change. Submit type used for this change, can be `MERGE_IF_NECESSARY`, `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or `CHERRY_PICK`. +|`merge_strategy` |optional| +The strategy of the merge, can be `recursive`, `resolve`, +`simple-two-way-in-core`, `ours` or `theirs`. |`mergeable` || `true` if this change is cleanly mergeable, `false` otherwise +|`conflicts`|optional| +A list of paths with conflicts |`mergeable_into`|optional| A list of other branch names where this change could merge cleanly |============================ +[[merge-input]] +=== MergeInput +The `MergeInput` entity contains information about the merge + +[options="header",cols="1,^1,5"] +|============================ +|Field Name ||Description +|`source` || +The source to merge from, e.g. a complete or abbreviated commit SHA-1, +a complete reference name, a short reference name under refs/heads, refs/tags, +or refs/remotes namespace, etc. +|`strategy` |optional| +The strategy of the merge, can be `recursive`, `resolve`, +`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings. +|============================ + [[move-input]] === MoveInput The `MoveInput` entity contains information for moving a change to a new branch. diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index 7d4b92f0c4..21a7ac7182 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt @@ -1354,6 +1354,42 @@ The content is returned as base64 encoded string. Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY... ---- + +[[get-mergeable-info]] +=== Get Mergeable Information +-- +'GET /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]/mergeable' +-- + +Gets whether the source is mergeable with the target branch. + +The `source` query parameter is required, which can be anything that could be resolved +to a commit, see examples in link:rest-api-changes.html#merge-input[MergeInput]. + +Also takes an optional parameter `strategy`, which can be `recursive`, `resolve`, +`simple-two-way-in-core`, `ours` or `theirs`, default will use project settings. + +.Request +---- + GET /projects/test/branches/master/mergeable?source=testbranch&strategy=recursive HTTP/1.0 +---- + +As response a link:#mergeable-info[MergeableInfo] entity is returned. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + submit_type: "MERGE_IF_NECESSARY", + merge_strategy: "recursive", + mergeable: true + } +---- + [[get-reflog]] === Get Reflog -- diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java index 7179e80c1b..a55acf24d7 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java @@ -206,6 +206,11 @@ public class PushOneCommit { } } + public void setParent(RevCommit parent) throws Exception { + commitBuilder.noParents(); + commitBuilder.parent(parent); + } + public Result to(String ref) throws Exception { commitBuilder.add(fileName, content); return execute(ref); diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java index 13f506385f..25628c22e5 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java @@ -22,14 +22,19 @@ import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.PushOneCommit.Result; import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInput; +import com.google.gerrit.extensions.common.MergeInput; +import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.config.AnonymousCowardNameProvider; import com.google.gerrit.testutil.ConfigSuite; @@ -191,4 +196,81 @@ public class CreateChangeIT extends AbstractDaemonTest { assertThat(o.signedOffBy).isTrue(); } + + + @Test + public void createMergeChange() throws Exception { + changeInTwoBranches("a.txt", "b.txt"); + ChangeInput in = + newMergeChangeInput("master", "branchA", ChangeStatus.NEW); + assertCreateSucceeds(in); + } + + @Test + public void createMergeChange_Conflicts() throws Exception { + changeInTwoBranches("shared.txt", "shared.txt"); + ChangeInput in = + newMergeChangeInput("master", "branchA", ChangeStatus.NEW); + assertCreateFails(in, RestApiException.class, "merge conflict"); + } + + @Test + public void dryRunMerge() throws Exception { + changeInTwoBranches("a.txt", "b.txt"); + RestResponse r = adminRestSession.get("/projects/" + project.get() + + "/branches/master/mergeable?source=branchA"); + MergeableInfo m = newGson().fromJson(r.getReader(), MergeableInfo.class); + assertThat(m.mergeable).isTrue(); + } + + @Test + public void dryRunMerge_Conflicts() throws Exception { + changeInTwoBranches("a.txt", "a.txt"); + RestResponse r = adminRestSession.get("/projects/" + project.get() + + "/branches/master/mergeable?source=branchA"); + MergeableInfo m = newGson().fromJson(r.getReader(), MergeableInfo.class); + assertThat(m.mergeable).isFalse(); + assertThat(m.conflicts).containsExactly("a.txt"); + } + + private ChangeInput newMergeChangeInput(String targetBranch, + String sourceRef, ChangeStatus status) { + // create a merge change from branchA to master in gerrit + ChangeInput in = new ChangeInput(); + in.project = project.get(); + in.branch = targetBranch; + in.subject = "merge " + sourceRef + " to " + targetBranch; + in.status = status; + MergeInput mergeInput = new MergeInput(); + mergeInput.source = sourceRef; + in.merge = mergeInput; + return in; + } + + private void changeInTwoBranches(String fileInMaster, String fileInBranchA) + throws Exception { + // create a initial commit in master + Result initialCommit = pushFactory + .create(db, user.getIdent(), testRepo, "initial commit", "readme.txt", + "initial commit") + .to("refs/heads/master"); + initialCommit.assertOkStatus(); + + // create a new branch branchA + createBranch(new Branch.NameKey(project, "branchA")); + + // create another commit in master + Result changeToMaster = pushFactory + .create(db, user.getIdent(), testRepo, "change to master", fileInMaster, + "master content") + .to("refs/heads/master"); + changeToMaster.assertOkStatus(); + + // create a commit in branchA + PushOneCommit commit = pushFactory + .create(db, user.getIdent(), testRepo, "change to branchA", + fileInBranchA, "branchA content"); + commit.setParent(initialCommit.getCommit()); + commit.to("refs/heads/branchA"); + } } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java index 9f0df931a9..88c3ea8ce6 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java @@ -25,4 +25,5 @@ public class ChangeInput { public ChangeStatus status; public String baseChange; public Boolean newBranch; + public MergeInput merge; } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java new file mode 100644 index 0000000000..598d61899a --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeInput.java @@ -0,0 +1,31 @@ +// Copyright (C) 2016 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.extensions.common; + +public class MergeInput { + /** + * {@code source} can be any Git object reference expression. + * + * @see gitrevisions(7) + */ + public String source; + + /** + * {@code strategy} name of the merge strategy. + * + * @see org.eclipse.jgit.merge.MergeStrategy + */ + public String strategy; +} diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java index 9c38055e56..16091a30d0 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/MergeableInfo.java @@ -20,6 +20,8 @@ import java.util.List; public class MergeableInfo { public SubmitType submitType; + public String strategy; public boolean mergeable; + public List conflicts; public List mergeableInto; } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java index 6d8a2e0bdb..8f94c6bf37 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java @@ -16,6 +16,7 @@ package com.google.gerrit.server.change; import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG; +import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.gerrit.common.TimeUtil; @@ -24,6 +25,7 @@ import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInput; +import com.google.gerrit.extensions.common.MergeInput; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; @@ -50,10 +52,12 @@ import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.BatchUpdate; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MergeUtil; import com.google.gerrit.server.git.UpdateException; import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectResource; import com.google.gerrit.server.project.ProjectsCollection; import com.google.gerrit.server.project.RefControl; @@ -99,6 +103,7 @@ public class CreateChange implements private final BatchUpdate.Factory updateFactory; private final PatchSetUtil psUtil; private final boolean allowDrafts; + private final MergeUtil.Factory mergeUtilFactory; @Inject CreateChange(@AnonymousCowardName String anonymousCowardName, @@ -114,7 +119,8 @@ public class CreateChange implements ChangeFinder changeFinder, BatchUpdate.Factory updateFactory, PatchSetUtil psUtil, - @GerritServerConfig Config config) { + @GerritServerConfig Config config, + MergeUtil.Factory mergeUtilFactory) { this.anonymousCowardName = anonymousCowardName; this.db = db; this.gitManager = gitManager; @@ -129,6 +135,7 @@ public class CreateChange implements this.updateFactory = updateFactory; this.psUtil = psUtil; this.allowDrafts = config.getBoolean("change", "allowDrafts", true); + this.mergeUtilFactory = mergeUtilFactory; } @Override @@ -173,7 +180,8 @@ public class CreateChange implements Project.NameKey project = rsrc.getNameKey(); try (Repository git = gitManager.openRepository(project); - RevWalk rw = new RevWalk(git)) { + ObjectInserter oi = git.newObjectInserter(); + RevWalk rw = new RevWalk(oi.newReader())) { ObjectId parentCommit; List groups; if (input.baseChange != null) { @@ -219,42 +227,47 @@ public class CreateChange implements GeneralPreferencesInfo info = account.getAccount().getGeneralPreferencesInfo(); - try (ObjectInserter oi = git.newObjectInserter()) { - ObjectId treeId = - mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree(); - ObjectId id = ChangeIdUtil.computeChangeId(treeId, - mergeTip, author, author, input.subject); - String commitMessage = ChangeIdUtil.insertId(input.subject, id); - if (Boolean.TRUE.equals(info.signedOffBy)) { - commitMessage += String.format("%s%s", - SIGNED_OFF_BY_TAG, - account.getAccount().getNameEmail(anonymousCowardName)); - } - - RevCommit c = newCommit(oi, rw, author, mergeTip, commitMessage); - - Change.Id changeId = new Change.Id(seq.nextChangeId()); - ChangeInserter ins = changeInserterFactory.create(changeId, c, refName) - .setValidatePolicy(CommitValidators.Policy.GERRIT); - ins.setMessage(String.format("Uploaded patch set %s.", - ins.getPatchSetId().get())); - String topic = input.topic; - if (topic != null) { - topic = Strings.emptyToNull(topic.trim()); - } - ins.setTopic(topic); - ins.setDraft(input.status != null && input.status == ChangeStatus.DRAFT); - ins.setGroups(groups); - try (BatchUpdate bu = updateFactory.create( - db.get(), project, me, now)) { - bu.setRepository(git, rw, oi); - bu.insertChange(ins); - bu.execute(); - } - ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS); - return Response.created(json.format(ins.getChange())); + ObjectId treeId = + mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree(); + ObjectId id = ChangeIdUtil.computeChangeId(treeId, + mergeTip, author, author, input.subject); + String commitMessage = ChangeIdUtil.insertId(input.subject, id); + if (Boolean.TRUE.equals(info.signedOffBy)) { + commitMessage += String.format("%s%s", + SIGNED_OFF_BY_TAG, + account.getAccount().getNameEmail(anonymousCowardName)); } + RevCommit c; + if (input.merge != null) { + // create a merge commit + c = newMergeCommit(git, oi, rw, rsrc.getControl(), mergeTip, input.merge, + author, commitMessage); + } else { + // create an empty commit + c = newCommit(oi, rw, author, mergeTip, commitMessage); + } + + Change.Id changeId = new Change.Id(seq.nextChangeId()); + ChangeInserter ins = changeInserterFactory.create(changeId, c, refName) + .setValidatePolicy(CommitValidators.Policy.GERRIT); + ins.setMessage(String.format("Uploaded patch set %s.", + ins.getPatchSetId().get())); + String topic = input.topic; + if (topic != null) { + topic = Strings.emptyToNull(topic.trim()); + } + ins.setTopic(topic); + ins.setDraft(input.status == ChangeStatus.DRAFT); + ins.setGroups(groups); + try (BatchUpdate bu = updateFactory.create( + db.get(), project, me, now)) { + bu.setRepository(git, rw, oi); + bu.insertChange(ins); + bu.execute(); + } + ChangeJson json = jsonFactory.create(ChangeJson.NO_OPTIONS); + return Response.created(json.format(ins.getChange())); } } @@ -274,6 +287,32 @@ public class CreateChange implements return rw.parseCommit(insert(oi, commit)); } + private RevCommit newMergeCommit(Repository repo, ObjectInserter oi, + RevWalk rw, ProjectControl projectControl, RevCommit mergeTip, + MergeInput merge, PersonIdent authorIdent, String commitMessage) + throws RestApiException, IOException { + if (Strings.isNullOrEmpty(merge.source)) { + throw new BadRequestException("merge.source must be non-empty"); + } + + RevCommit sourceCommit = MergeUtil.resolveCommit(repo, rw, merge.source); + if (!projectControl.canReadCommit(db.get(), repo, sourceCommit)) { + throw new BadRequestException( + "do not have read permission for: " + merge.source); + } + + MergeUtil mergeUtil = + mergeUtilFactory.create(projectControl.getProjectState()); + // default merge strategy from project settings + String mergeStrategy = MoreObjects.firstNonNull( + Strings.emptyToNull(merge.strategy), + mergeUtil.mergeStrategyName()); + + return rw.parseCommit(MergeUtil + .createMergeCommit(repo, oi, mergeTip, sourceCommit, mergeStrategy, + authorIdent, commitMessage, rw)); + } + private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit) throws IOException, UnsupportedEncodingException { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java index 08ef76e6e1..aacc8aaeff 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java @@ -41,6 +41,7 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.merge.MergeStrategy; import org.kohsuke.args4j.Option; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -110,6 +111,7 @@ public class Mergeable implements RestReadView { ProjectState projectState = projectCache.get(change.getProject()); String strategy = mergeUtilFactory.create(projectState) .mergeStrategyName(); + result.strategy = strategy; result.mergeable = isMergable(git, change, commit, ref, result.submitType, strategy); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java index 109fa76ff3..e560034e8d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java @@ -14,9 +14,16 @@ package com.google.gerrit.server.git; -/** Indicates that the commit is already contained in destination banch. */ -public class MergeIdenticalTreeException extends Exception { +import com.google.gerrit.extensions.restapi.RestApiException; + +/** + * Indicates that the commit is already contained in destination branch. + * + */ +public class MergeIdenticalTreeException extends RestApiException { private static final long serialVersionUID = 1L; + + /** @param msg message to return to the client describing the error. */ public MergeIdenticalTreeException(String msg) { super(msg, null); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java index 7c36961e16..e30550e06a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java @@ -19,12 +19,15 @@ import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MergeConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; @@ -45,11 +48,13 @@ import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; +import org.eclipse.jgit.errors.AmbiguousObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NoMergeBaseException; import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason; +import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Config; @@ -60,6 +65,7 @@ import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.Merger; +import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.merge.ThreeWayMergeStrategy; import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.FooterKey; @@ -202,6 +208,44 @@ public class MergeUtil { throw new MergeConflictException("merge conflict"); } + public static ObjectId createMergeCommit(Repository repo, ObjectInserter inserter, + RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy, + PersonIdent committerIndent, String commitMsg, RevWalk rw) + throws IOException, MergeIdenticalTreeException, MergeConflictException { + + if (rw.isMergedInto(originalCommit, mergeTip)) { + throw new MergeIdenticalTreeException( + "merge identical tree: change(s) has been already merged!"); + } + + Merger m = newMerger(repo, inserter, mergeStrategy); + if (m.merge(false, mergeTip, originalCommit)) { + ObjectId tree = m.getResultTreeId(); + + CommitBuilder mergeCommit = new CommitBuilder(); + mergeCommit.setTreeId(tree); + mergeCommit.setParentIds(mergeTip, originalCommit); + mergeCommit.setAuthor(committerIndent); + mergeCommit.setCommitter(committerIndent); + mergeCommit.setMessage(commitMsg); + return inserter.insert(mergeCommit); + } else { + List conflicts = ImmutableList.of(); + if (m instanceof ResolveMerger) { + conflicts = ((ResolveMerger) m).getUnmergedPaths(); + } + throw new MergeConflictException(createConflictMessage(conflicts)); + } + } + + public static String createConflictMessage(List conflicts) { + StringBuilder sb = new StringBuilder("merge conflict(s)"); + for (String c : conflicts) { + sb.append('\n' + c); + } + return sb.toString(); + } + public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl, PatchSet.Id psId) { Change c = ctl.getChange(); @@ -591,11 +635,17 @@ public class MergeUtil { public static ThreeWayMerger newThreeWayMerger(Repository repo, final ObjectInserter inserter, String strategyName) { + Merger m = newMerger(repo, inserter, strategyName); + checkArgument(m instanceof ThreeWayMerger, + "merge strategy %s does not support three-way merging", strategyName); + return (ThreeWayMerger) m; + } + + public static Merger newMerger(Repository repo, + final ObjectInserter inserter, String strategyName) { MergeStrategy strategy = MergeStrategy.get(strategyName); checkArgument(strategy != null, "invalid merge strategy: %s", strategyName); Merger m = strategy.newMerger(repo, true); - checkArgument(m instanceof ThreeWayMerger, - "merge strategy %s does not support three-way merging", strategyName); m.setObjectInserter(new ObjectInserter.Filter() { @Override protected ObjectInserter delegate() { @@ -610,7 +660,7 @@ public class MergeUtil { public void close() { } }); - return (ThreeWayMerger) m; + return m; } public void markCleanMerges(final RevWalk rw, @@ -693,4 +743,16 @@ public class MergeUtil { } return null; } + + public static RevCommit resolveCommit(Repository repo, RevWalk rw, String str) + throws BadRequestException, ResourceNotFoundException, IOException { + try { + return rw.parseCommit(repo.resolve(str)); + } catch (AmbiguousObjectException | IncorrectObjectTypeException | + RevisionSyntaxException e) { + throw new BadRequestException(e.getMessage()); + } catch (MissingObjectException e) { + throw new ResourceNotFoundException(e.getMessage()); + } + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java new file mode 100644 index 0000000000..fd4e359b64 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java @@ -0,0 +1,107 @@ +// Copyright (C) 2016 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.project; + +import com.google.gerrit.extensions.common.MergeableInfo; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.InMemoryInserter; +import com.google.gerrit.server.git.MergeUtil; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.merge.Merger; +import org.eclipse.jgit.merge.ResolveMerger; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.kohsuke.args4j.Option; + +import java.io.IOException; + +/** + * Check the mergeability at current branch for a git object references expression. + */ +public class CheckMergeability implements RestReadView { + + private String source; + private String strategy; + private final Provider db; + + @Option(name = "--source", metaVar = "COMMIT", + usage = "the source reference to merge, which could be any git object " + + "references expression, refer to " + + "org.eclipse.jgit.lib.Repository#resolve(String)", + required = true) + public void setSource(String source) { + this.source = source; + } + + @Option(name = "--strategy", metaVar = "STRATEGY", + usage = "name of the merge strategy, refer to " + + "org.eclipse.jgit.merge.MergeStrategy") + public void setStrategy(String strategy) { + this.strategy = strategy; + } + + private final GitRepositoryManager gitManager; + + @Inject + CheckMergeability(GitRepositoryManager gitManager, + @GerritServerConfig Config cfg, + Provider db) { + this.gitManager = gitManager; + this.strategy = MergeUtil.getMergeStrategy(cfg).getName(); + this.db = db; + } + + @Override + public MergeableInfo apply(BranchResource resource) + throws IOException, BadRequestException, ResourceNotFoundException { + MergeableInfo result = new MergeableInfo(); + result.strategy = strategy; + try (Repository git = gitManager.openRepository(resource.getNameKey()); + RevWalk rw = new RevWalk(git); + ObjectInserter inserter = new InMemoryInserter(git)) { + Merger m = MergeUtil.newMerger(git, inserter, strategy); + + Ref destRef = git.getRefDatabase().exactRef(resource.getRef()); + if (destRef == null) { + throw new ResourceNotFoundException(resource.getRef()); + } + + RevCommit targetCommit = rw.parseCommit(destRef.getObjectId()); + RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, source); + + if (!resource.getControl().canReadCommit(db.get(), git, sourceCommit)) { + throw new BadRequestException( + "do not have read permission for: " + source); + } + + result.mergeable = m.merge(targetCommit, sourceCommit); + if (m instanceof ResolveMerger) { + result.conflicts = ((ResolveMerger) m).getUnmergedPaths(); + } + } + return result; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java index 341d741ad7..970629823e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java @@ -68,6 +68,7 @@ public class Module extends RestApiModule { delete(BRANCH_KIND).to(DeleteBranch.class); post(PROJECT_KIND, "branches:delete").to(DeleteBranches.class); factory(CreateBranch.Factory.class); + get(BRANCH_KIND, "mergeable").to(CheckMergeability.class); get(BRANCH_KIND, "reflog").to(GetReflog.class); child(BRANCH_KIND, "files").to(FilesCollection.class); get(FILE_KIND, "content").to(GetContent.class);