Support rebase via REST

POST on '/changes/<change-id>/rebase' rebases a change via REST.

POST on '/changes/<change-id>/revisions/<revision-id>/rebase' rebases a
revision via REST.

Change-Id: I573f914e005ea75125530bee6f8a9f37036e3563
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2013-03-15 15:06:59 +01:00
parent 7759586450
commit cdae63bd1a
7 changed files with 324 additions and 9 deletions

View File

@@ -710,6 +710,95 @@ the error message is contained in the response body.
change is new
----
[[rebase-change]]
Rebase Change
~~~~~~~~~~~~~
[verse]
'POST /changes/link:#change-id[\{change-id\}]/rebase'
Rebases a change.
.Request
----
POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/rebase HTTP/1.0
----
As response a link:#change-info[ChangeInfo] entity is returned that
describes the rebased change. Information about the current patch set
is included.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json;charset=UTF-8
)]}'
{
"kind": "gerritcodereview#change",
"id": "myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
"project": "myProject",
"branch": "master",
"change_id": "I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
"subject": "Implement Feature X",
"status": "NEW",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
"mergeable": false,
"_sortkey": "0024cf9a000012bf",
"_number": 4799,
"owner": {
"name": "John Doe"
},
"current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
"revisions": {
"27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
"_number": 2,
"fetch": {
"http": {
"url": "http://gerrit:8080/myProject",
"ref": "refs/changes/99/4799/2"
}
},
"commit": {
"parents": [
{
"commit": "b4003890dadd406d80222bf1ad8aca09a4876b70",
"subject": "Implement Feature A"
}
],
"author": {
"name": "John Doe",
"email": "john.doe@example.com",
"date": "2013-05-07 15:21:27.000000000",
"tz": 120
},
"committer": {
"name": "Gerrit Code Review",
"email": "gerrit-server@example.com",
"date": "2013-05-07 15:35:43.000000000",
"tz": 120
},
"subject": "Implement Feature X",
"message": "Implement Feature X\n\nAdded feature X."
}
}
}
----
If the change cannot be rebased, e.g. due to conflicts, the response is
"`409 Conflict`" and the error message is contained in the response
body.
.Response
----
HTTP/1.1 409 Conflict
Content-Disposition: attachment
Content-Type: text/plain;charset=UTF-8
The change could not be rebased due to a path conflict during merge.
----
[[revert-change]]
Revert Change
~~~~~~~~~~~~~
@@ -1218,6 +1307,95 @@ describes the applied labels.
}
----
[[rebase-revision]]
Rebase Revision
~~~~~~~~~~~~~~~
[verse]
'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/rebase'
Rebases a revision.
.Request
----
POST /changes/myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/rebase HTTP/1.0
----
As response a link:#change-info[ChangeInfo] entity is returned that
describes the rebased change. Information about the current patch set
is included.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json;charset=UTF-8
)]}'
{
"kind": "gerritcodereview#change",
"id": "myProject~master~I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
"project": "myProject",
"branch": "master",
"change_id": "I3ea943139cb62e86071996f2480e58bf3eeb9dd2",
"subject": "Implement Feature X",
"status": "NEW",
"created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000",
"mergeable": false,
"_sortkey": "0024cf9a000012bf",
"_number": 4799,
"owner": {
"name": "John Doe"
},
"current_revision": "27cc4558b5a3d3387dd11ee2df7a117e7e581822",
"revisions": {
"27cc4558b5a3d3387dd11ee2df7a117e7e581822": {
"_number": 2,
"fetch": {
"http": {
"url": "http://gerrit:8080/myProject",
"ref": "refs/changes/99/4799/2"
}
},
"commit": {
"parents": [
{
"commit": "b4003890dadd406d80222bf1ad8aca09a4876b70",
"subject": "Implement Feature A"
}
],
"author": {
"name": "John Doe",
"email": "john.doe@example.com",
"date": "2013-05-07 15:21:27.000000000",
"tz": 120
},
"committer": {
"name": "Gerrit Code Review",
"email": "gerrit-server@example.com",
"date": "2013-05-07 15:35:43.000000000",
"tz": 120
},
"subject": "Implement Feature X",
"message": "Implement Feature X\n\nAdded feature X."
}
}
}
----
If the revision cannot be rebased, e.g. due to conflicts, the response is
"`409 Conflict`" and the error message is contained in the response
body.
.Response
----
HTTP/1.1 409 Conflict
Content-Disposition: attachment
Content-Type: text/plain;charset=UTF-8
The change could not be rebased due to a path conflict during merge.
----
[[submit-revision]]
Submit Revision
~~~~~~~~~~~~~~~

View File

@@ -62,7 +62,7 @@ class RebaseChangeHandler extends Handler<ChangeDetail> {
EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
MissingObjectException, IncorrectObjectTypeException, IOException,
InvalidChangeOperationException, NoSuchProjectException {
rebaseChange.rebase(patchSetId, currentUser.getAccountId());
rebaseChange.rebase(patchSetId, currentUser);
return changeDetailFactory.create(patchSetId.getParentKey()).call();
}
}

View File

@@ -53,6 +53,7 @@ public class Module extends RestApiModule {
post(CHANGE_KIND, "restore").to(Restore.class);
post(CHANGE_KIND, "revert").to(Revert.class);
post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
child(CHANGE_KIND, "reviewers").to(Reviewers.class);
@@ -63,6 +64,7 @@ public class Module extends RestApiModule {
get(REVISION_KIND, "review").to(GetReview.class);
post(REVISION_KIND, "review").to(PostReview.class);
post(REVISION_KIND, "submit").to(Submit.class);
post(REVISION_KIND, "rebase").to(Rebase.class);
get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);

View File

@@ -0,0 +1,105 @@
// Copyright (C) 2013 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.common.changes.ListChangesOption;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
import com.google.gerrit.server.change.Rebase.Input;
import com.google.gerrit.server.changedetail.RebaseChange;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
public class Rebase implements RestModifyView<RevisionResource, Input> {
public static class Input {
}
private final RebaseChange rebaseChange;
private final ChangeJson json;
@Inject
public Rebase(RebaseChange rebaseChange, ChangeJson json) {
this.rebaseChange = rebaseChange;
this.json = json;
}
@Override
public ChangeInfo apply(RevisionResource rsrc, Input input)
throws AuthException, ResourceNotFoundException,
ResourceConflictException, EmailException, OrmException {
ChangeControl control = rsrc.getControl();
Change change = rsrc.getChange();
if (!control.canRebase()) {
throw new AuthException("rebase not permitted");
} else if (!change.getStatus().isOpen()) {
throw new ResourceConflictException("change is "
+ change.getStatus().name().toLowerCase());
}
try {
rebaseChange.rebase(rsrc.getPatchSet().getId(), rsrc.getUser());
} catch (InvalidChangeOperationException e) {
throw new ResourceConflictException(e.getMessage());
} catch (IOException e) {
throw new ResourceConflictException(e.getMessage());
} catch (NoSuchChangeException e) {
throw new ResourceNotFoundException(change.getId().toString());
}
json.addOption(ListChangesOption.CURRENT_REVISION)
.addOption(ListChangesOption.CURRENT_COMMIT);
return json.format(change.getId());
}
public static class CurrentRevision implements
RestModifyView<ChangeResource, Input> {
private final Provider<ReviewDb> dbProvider;
private final Rebase rebase;
@Inject
CurrentRevision(Provider<ReviewDb> dbProvider, Rebase rebase) {
this.dbProvider = dbProvider;
this.rebase = rebase;
}
@Override
public ChangeInfo apply(ChangeResource rsrc, Input input)
throws AuthException, ResourceNotFoundException,
ResourceConflictException, EmailException, OrmException {
PatchSet ps =
dbProvider.get().patchSets()
.get(rsrc.getChange().currentPatchSetId());
if (ps == null) {
throw new ResourceConflictException("current revision is missing");
} else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
throw new AuthException("current revision not accessible");
}
return rebase.apply(new RevisionResource(rsrc, ps), input);
}
}
}

View File

@@ -48,6 +48,10 @@ public class RevisionResource implements RestResource {
}
Account.Id getAccountId() {
return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
return getUser().getAccountId();
}
IdentifiedUser getUser() {
return (IdentifiedUser) getControl().getCurrentUser();
}
}

View File

@@ -31,6 +31,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
@@ -64,7 +65,7 @@ import java.util.List;
import java.util.Set;
public class RebaseChange {
private final ChangeControl.Factory changeControlFactory;
private final ChangeControl.GenericFactory changeControlFactory;
private final PatchSetInfoFactory patchSetInfoFactory;
private final ReviewDb db;
private final GitRepositoryManager gitManager;
@@ -76,7 +77,7 @@ public class RebaseChange {
private final ProjectCache projectCache;
@Inject
RebaseChange(final ChangeControl.Factory changeControlFactory,
RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
final PatchSetInfoFactory patchSetInfoFactory, final ReviewDb db,
@GerritPersonIdent final PersonIdent myIdent,
final GitRepositoryManager gitManager,
@@ -123,12 +124,12 @@ public class RebaseChange {
* @throws IOException thrown if rebase is not possible or not needed
* @throws InvalidChangeOperationException thrown if rebase is not allowed
*/
public void rebase(final PatchSet.Id patchSetId, final Account.Id uploader)
public void rebase(final PatchSet.Id patchSetId, final IdentifiedUser uploader)
throws NoSuchChangeException, EmailException, OrmException, IOException,
InvalidChangeOperationException {
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl changeControl =
changeControlFactory.validateFor(changeId);
changeControlFactory.validateFor(changeId, uploader);
if (!changeControl.canRebase()) {
throw new InvalidChangeOperationException(
"Cannot rebase: New patch sets are not allowed to be added to change: "
@@ -152,7 +153,7 @@ public class RebaseChange {
rw.parseCommit(ObjectId.fromString(baseRev));
final PatchSet newPatchSet =
rebase(git, rw, inserter, patchSetId, change, uploader, baseCommit,
rebase(git, rw, inserter, patchSetId, change, uploader.getAccountId(), baseCommit,
mergeUtilFactory.create(
changeControl.getProjectControl().getProjectState(), true));
@@ -167,7 +168,7 @@ public class RebaseChange {
}
final ReplacePatchSetSender cm =
rebasedPatchSetSenderFactory.create(change);
cm.setFrom(uploader);
cm.setFrom(uploader.getAccountId());
cm.setPatchSet(newPatchSet);
cm.addReviewers(oldReviewers);
cm.addExtraCC(oldCC);

View File

@@ -56,10 +56,12 @@ public class ChangeControl {
public static class GenericFactory {
private final ProjectControl.GenericFactory projectControl;
private final Provider<ReviewDb> db;
@Inject
GenericFactory(ProjectControl.GenericFactory p) {
GenericFactory(ProjectControl.GenericFactory p, Provider<ReviewDb> d) {
projectControl = p;
db = d;
}
public ChangeControl controlFor(Change change, CurrentUser user)
@@ -71,6 +73,29 @@ public class ChangeControl {
throw new NoSuchChangeException(change.getId(), e);
}
}
public ChangeControl controlFor(Change.Id id, CurrentUser user)
throws NoSuchChangeException {
final Change change;
try {
change = db.get().changes().get(id);
if (change == null) {
throw new NoSuchChangeException(id);
}
} catch (OrmException e) {
throw new NoSuchChangeException(id, e);
}
return controlFor(change, user);
}
public ChangeControl validateFor(Change.Id id, CurrentUser user)
throws NoSuchChangeException, OrmException {
ChangeControl c = controlFor(id, user);
if (!c.isVisible(db.get())) {
throw new NoSuchChangeException(c.getChange().getId());
}
return c;
}
}
public static class Factory {