Add patch set description api endpoints

Adds GET and PUT endpoints for the patch set description.

Feature: Issue 4544
Change-Id: I8b6d1bd46b711212f63d2b9e0847b8872a1df706
This commit is contained in:
Kasper Nilsson
2016-11-15 11:12:37 -08:00
parent 26ed9f20d2
commit 9f2ed6a847
10 changed files with 311 additions and 4 deletions

View File

@@ -2792,6 +2792,64 @@ describes the revision.
Adding query parameter `links` (for example `/changes/.../commit?links`)
returns a link:#commit-info[CommitInfo] with the additional field `web_links`.
[[get-description]]
=== Get Description
--
'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/description'
--
Retrieves the description of a patch set.
.Request
----
GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/description HTTP/1.0
----
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
"Added Documentation"
----
If the patch set does not have a description an empty string is returned.
[[set-description]]
=== Set Description
--
'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/description'
--
Sets the description of a patch set.
The new description must be provided in the request body inside a
link:#description-input[DescriptionInput] entity.
.Request
----
PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/description HTTP/1.0
Content-Type: application/json; charset=UTF-8
{
"description": "Added Documentation"
}
----
As response the new description is returned.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
"Added Documentation"
----
[[get-merge-list]]
=== Get Merge List
--
@@ -5097,6 +5155,16 @@ Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
If not set, the default is `ALL`.
|=======================
[[description-input]]
=== DescriptionInput
The `DescriptionInput` entity contains information for setting a description.
[options="header",cols="1,6"]
|===========================
|Field Name |Description
|`description` |The description text.
|===========================
[[diff-content]]
=== DiffContent
The `DiffContent` entity contains information about the content differences

View File

@@ -770,6 +770,31 @@ public class RevisionIT extends AbstractDaemonTest {
assertThat(diff.metaB.lines).isEqualTo(1);
}
@Test
public void description() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.description()).isEqualTo("");
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.description("test");
assertThat(gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.description()).isEqualTo("test");
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.description("");
assertThat(gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().name())
.description()).isEqualTo("");
}
@Test
public void content() throws Exception {
PushOneCommit.Result r = createChange();
@@ -946,11 +971,11 @@ public class RevisionIT extends AbstractDaemonTest {
public void actions() throws Exception {
PushOneCommit.Result r = createChange();
assertThat(current(r).actions().keySet())
.containsExactly("cherrypick", "rebase");
.containsExactly("cherrypick", "description", "rebase");
current(r).review(ReviewInput.approve());
assertThat(current(r).actions().keySet())
.containsExactly("submit", "cherrypick", "rebase");
.containsExactly("submit", "cherrypick", "description", "rebase");
current(r).submit();
assertThat(current(r).actions().keySet())

View File

@@ -84,9 +84,10 @@ public class ActionsIT extends AbstractDaemonTest {
public void revisionActionsOneChangePerTopicUnapproved() throws Exception {
String changeId = createChangeWithTopic().getChangeId();
Map<String, ActionInfo> actions = getActions(changeId);
assertThat(actions).hasSize(3);
assertThat(actions).containsKey("cherrypick");
assertThat(actions).containsKey("rebase");
assertThat(actions).hasSize(2);
assertThat(actions).containsKey("description");
}
@Test
@@ -436,9 +437,10 @@ public class ActionsIT extends AbstractDaemonTest {
}
private void commonActionsAssertions(Map<String, ActionInfo> actions) {
assertThat(actions).hasSize(3);
assertThat(actions).hasSize(4);
assertThat(actions).containsKey("cherrypick");
assertThat(actions).containsKey("submit");
assertThat(actions).containsKey("description");
assertThat(actions).containsKey("rebase");
}

View File

@@ -33,6 +33,9 @@ import java.util.Set;
public interface RevisionApi {
void delete() throws RestApiException;
String description() throws RestApiException;
void description(String description) throws RestApiException;
void review(ReviewInput in) throws RestApiException;
void submit() throws RestApiException;
@@ -283,5 +286,15 @@ public interface RevisionApi {
public MergeListRequest getMergeList() throws RestApiException {
throw new NotImplementedException();
}
@Override
public void description(String description) throws RestApiException {
throw new NotImplementedException();
}
@Override
public String description() throws RestApiException {
throw new NotImplementedException();
}
}
}

View File

@@ -64,6 +64,8 @@ public class ChangeMessagesUtil {
"autogenerated:gerrit:revert";
public static final String TAG_SET_ASSIGNEE =
"autogenerated:gerrit:setAssignee";
public static final String TAG_SET_DESCRIPTION =
"autogenerated:gerrit:setPsDescription";
public static final String TAG_SET_HASHTAGS =
"autogenerated:gerrit:setHashtag";
public static final String TAG_SET_TOPIC =

View File

@@ -47,6 +47,7 @@ import com.google.gerrit.server.change.DeleteDraftPatchSet;
import com.google.gerrit.server.change.DraftComments;
import com.google.gerrit.server.change.FileResource;
import com.google.gerrit.server.change.Files;
import com.google.gerrit.server.change.GetDescription;
import com.google.gerrit.server.change.GetMergeList;
import com.google.gerrit.server.change.GetPatch;
import com.google.gerrit.server.change.GetRevisionActions;
@@ -57,6 +58,7 @@ import com.google.gerrit.server.change.Mergeable;
import com.google.gerrit.server.change.PostReview;
import com.google.gerrit.server.change.PreviewSubmit;
import com.google.gerrit.server.change.PublishDraftPatchSet;
import com.google.gerrit.server.change.PutDescription;
import com.google.gerrit.server.change.Rebase;
import com.google.gerrit.server.change.RebaseUtil;
import com.google.gerrit.server.change.Reviewed;
@@ -118,6 +120,8 @@ class RevisionApiImpl implements RevisionApi {
private final TestSubmitType testSubmitType;
private final TestSubmitType.Get getSubmitType;
private final Provider<GetMergeList> getMergeList;
private final PutDescription putDescription;
private final GetDescription getDescription;
@Inject
RevisionApiImpl(GitRepositoryManager repoManager,
@@ -151,6 +155,8 @@ class RevisionApiImpl implements RevisionApi {
TestSubmitType testSubmitType,
TestSubmitType.Get getSubmitType,
Provider<GetMergeList> getMergeList,
PutDescription putDescription,
GetDescription getDescription,
@Assisted RevisionResource r) {
this.repoManager = repoManager;
this.changes = changes;
@@ -183,6 +189,8 @@ class RevisionApiImpl implements RevisionApi {
this.testSubmitType = testSubmitType;
this.getSubmitType = getSubmitType;
this.getMergeList = getMergeList;
this.putDescription = putDescription;
this.getDescription = getDescription;
this.revision = r;
}
@@ -515,4 +523,20 @@ class RevisionApiImpl implements RevisionApi {
}
};
}
@Override
public void description(String description) throws RestApiException {
PutDescription.Input in = new PutDescription.Input();
in.description = description;
try {
putDescription.apply(revision, in);
} catch (UpdateException e) {
throw new RestApiException("Cannot set description", e);
}
}
@Override
public String description() throws RestApiException {
return getDescription.apply(revision);
}
}

View File

@@ -0,0 +1,27 @@
// 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.change;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.inject.Singleton;
@Singleton
public class GetDescription implements RestReadView<RevisionResource> {
@Override
public String apply(RevisionResource rsrc) {
return Strings.nullToEmpty(rsrc.getPatchSet().getDescription());
}
}

View File

@@ -104,6 +104,8 @@ public class Module extends RestApiModule {
get(REVISION_KIND, "preview_submit").to(PreviewSubmit.class);
post(REVISION_KIND, "submit").to(Submit.class);
post(REVISION_KIND, "rebase").to(Rebase.class);
put(REVISION_KIND, "description").to(PutDescription.class);
get(REVISION_KIND, "description").to(GetDescription.class);
get(REVISION_KIND, "patch").to(GetPatch.class);
get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);

View File

@@ -0,0 +1,132 @@
// 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.change;
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Collections;
@Singleton
public class PutDescription implements RestModifyView<RevisionResource,
PutDescription.Input>, UiAction<RevisionResource> {
private final Provider<ReviewDb> dbProvider;
private final ChangeMessagesUtil cmUtil;
private final BatchUpdate.Factory batchUpdateFactory;
private final PatchSetUtil psUtil;
public static class Input {
@DefaultInput
public String description;
}
@Inject
PutDescription(Provider<ReviewDb> dbProvider,
ChangeMessagesUtil cmUtil,
BatchUpdate.Factory batchUpdateFactory,
PatchSetUtil psUtil) {
this.dbProvider = dbProvider;
this.cmUtil = cmUtil;
this.batchUpdateFactory = batchUpdateFactory;
this.psUtil = psUtil;
}
@Override
public Response<String> apply(RevisionResource rsrc, Input input)
throws UpdateException, RestApiException {
ChangeControl ctl = rsrc.getControl();
if (!ctl.canEditDescription()) {
throw new AuthException("changing description not permitted");
}
Op op =
new Op(input != null ? input : new Input(), rsrc.getPatchSet().getId());
try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
rsrc.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
u.addOp(rsrc.getChange().getId(), op);
u.execute();
}
return Strings.isNullOrEmpty(op.newDescription) ? Response.none()
: Response.ok(op.newDescription);
}
private class Op extends BatchUpdate.Op {
private final Input input;
private final PatchSet.Id psId;
private String oldDescription;
private String newDescription;
Op(Input input, PatchSet.Id psId) {
this.input = input;
this.psId = psId;
}
@Override
public boolean updateChange(ChangeContext ctx) throws OrmException {
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
ChangeUpdate update = ctx.getUpdate(psId);
newDescription = Strings.nullToEmpty(input.description);
oldDescription = Strings.nullToEmpty(ps.getDescription());
if (oldDescription.equals(newDescription)) {
return false;
}
String summary;
if (oldDescription.isEmpty()) {
summary = "Description set to \"" + newDescription + "\"";
} else if (newDescription.isEmpty()) {
summary = "Description \"" + oldDescription + "\" removed";
} else {
summary = "Description changed to \"" + newDescription + "\"";
}
ps.setDescription(newDescription);
update.setPsDescription(newDescription);
ctx.getDb().patchSets().update(Collections.singleton(ps));
ChangeMessage cmsg =
ChangeMessagesUtil.newMessage(ctx.getDb(), psId, ctx.getUser(),
ctx.getWhen(), summary, ChangeMessagesUtil.TAG_SET_DESCRIPTION);
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
return true;
}
}
@Override
public UiAction.Description getDescription(RevisionResource rsrc) {
return new UiAction.Description().setLabel("Edit Description")
.setVisible(rsrc.getControl().canEditDescription());
}
}

View File

@@ -441,6 +441,18 @@ public class ChangeControl {
return getRefControl().canForceEditTopicName();
}
/** Can this user edit the description? */
public boolean canEditDescription() {
if (getChange().getStatus().isOpen()) {
return isOwner() // owner (aka creator) of the change can edit desc
|| getRefControl().isOwner() // branch owner can edit desc
|| getProjectControl().isOwner() // project owner can edit desc
|| getUser().getCapabilities().canAdministrateServer() // site administers are god
;
}
return false;
}
public boolean canEditAssignee() {
return isOwner()
|| getProjectControl().isOwner()