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
			
			
This commit is contained in:
		| @@ -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. | ||||
|   | ||||
| @@ -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 | ||||
| -- | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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"); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -25,4 +25,5 @@ public class ChangeInput { | ||||
|   public ChangeStatus status; | ||||
|   public String baseChange; | ||||
|   public Boolean newBranch; | ||||
|   public MergeInput merge; | ||||
| } | ||||
|   | ||||
| @@ -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 <a href="https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html">gitrevisions(7)</a> | ||||
|    */ | ||||
|   public String source; | ||||
|  | ||||
|   /** | ||||
|    * {@code strategy} name of the merge strategy. | ||||
|    * | ||||
|    * @see org.eclipse.jgit.merge.MergeStrategy | ||||
|    */ | ||||
|   public String strategy; | ||||
| } | ||||
| @@ -20,6 +20,8 @@ import java.util.List; | ||||
|  | ||||
| public class MergeableInfo { | ||||
|   public SubmitType submitType; | ||||
|   public String strategy; | ||||
|   public boolean mergeable; | ||||
|   public List<String> conflicts; | ||||
|   public List<String> mergeableInto; | ||||
| } | ||||
|   | ||||
| @@ -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<String> 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 { | ||||
|   | ||||
| @@ -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<RevisionResource> { | ||||
|       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); | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   } | ||||
|   | ||||
| @@ -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<String> conflicts = ImmutableList.of(); | ||||
|       if (m instanceof ResolveMerger) { | ||||
|         conflicts = ((ResolveMerger) m).getUnmergedPaths(); | ||||
|       } | ||||
|       throw new MergeConflictException(createConflictMessage(conflicts)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public static String createConflictMessage(List<String> 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()); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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<BranchResource> { | ||||
|  | ||||
|   private String source; | ||||
|   private String strategy; | ||||
|   private final Provider<ReviewDb> 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<ReviewDb> 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; | ||||
|   } | ||||
| } | ||||
| @@ -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); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Zhen Chen
					Zhen Chen