Support branch creation via REST

A new branch can be created by PUT on
'/projects/<project-name>/branches/<ref>.

The WebUI was adapted to use this new REST endpoint to create branches.
The old RPC for creating a branch in ProjectAdminService was deleted.

Change-Id: Id94bc4737eedde383507c7c567b3b6b102b105c5
This commit is contained in:
Edwin Kempin 2013-05-09 19:54:37 +02:00 committed by Shawn Pearce
parent 94a928b9a9
commit 5c0d6b33ff
11 changed files with 245 additions and 151 deletions

View File

@ -560,6 +560,44 @@ describes the branch.
}
----
[[create-branch]]
Create Branch
~~~~~~~~~~~~~
[verse]
'PUT /projects/link:#project-name[\{project-name\}]/branches/link:#branch-id[\{branch-id\}]'
Creates a new branch.
In the request body additional data for the branch can be provided as
link:#branch-input[BranchInput].
.Request
----
PUT /projects/MyProject/branches/stable HTTP/1.0
Content-Type: application/json;charset=UTF-8
{
"revision": "76016386a0d8ecc7b6be212424978bb45959d668"
}
----
As response a link:#branch-info[BranchInfo] entity is returned that
describes the created branch.
.Response
----
HTTP/1.1 201 Created
Content-Disposition: attachment
Content-Type: application/json;charset=UTF-8
)]}'
{
"ref": "refs/heads/stable",
"revision": "76016386a0d8ecc7b6be212424978bb45959d668",
"can_delete": true
}
----
[[child-project-endpoints]]
Child Project Endpoints
-----------------------
@ -968,6 +1006,24 @@ The `BranchInfo` entity contains information about a branch.
Whether the calling user can delete this branch.
|=========================
[[branch-input]]
BranchInput
~~~~~~~~~~~
The `BranchInput` entity contains information for the creation of
a new branch.
[options="header",width="50%",cols="1,^2,4"]
|=======================
|Field Name||Description
|`ref` |optional|
The name of the branch. The prefix `refs/heads/` can be
omitted. +
If set, must match the branch ID in the URL.
|`revision`|optional|
The base revision of the new branch. +
If not set, `HEAD` will be used as base revision.
|=======================
[[dashboard-info]]
DashboardInfo
~~~~~~~~~~~~~

View File

@ -56,11 +56,6 @@ public interface ProjectAdminService extends RemoteJsonService {
void listBranches(Project.NameKey projectName,
AsyncCallback<ListBranchesResult> callback);
@Audit
@SignInRequired
void addBranch(Project.NameKey projectName, String branchName,
String startingRevision, AsyncCallback<AddBranchResult> callback);
@Audit
@SignInRequired
void deleteBranch(Project.NameKey projectName, Set<Branch.NameKey> ids,

View File

@ -19,12 +19,13 @@ import com.google.gerrit.client.ConfirmationDialog;
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.projects.BranchInfo;
import com.google.gerrit.client.projects.ProjectApi;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.HintTextBox;
import com.google.gerrit.common.data.AddBranchResult;
import com.google.gerrit.common.data.ListBranchesResult;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
@ -185,62 +186,29 @@ public class ProjectBranchesScreen extends ProjectScreen {
}
addBranch.setEnabled(false);
Util.PROJECT_SVC.addBranch(getProjectKey(), branchName, rev,
new GerritCallback<AddBranchResult>() {
public void onSuccess(final AddBranchResult result) {
addBranch.setEnabled(true);
if (!result.hasError()) {
nameTxtBox.setText("");
irevTxtBox.setText("");
display(result.getListBranchesResult().getBranches());
} else {
final AddBranchResult.Error error = result.getError();
final String msg;
switch (error.getType()) {
case INVALID_NAME:
selectAllAndFocus(nameTxtBox);
msg = Gerrit.M.invalidBranchName(branchName);
break;
case INVALID_REVISION:
selectAllAndFocus(irevTxtBox);
msg = Gerrit.M.invalidRevision(rev);
break;
case BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX:
selectAllAndFocus(nameTxtBox);
msg =
Gerrit.M.branchCreationNotAllowedUnderRefnamePrefix(error
.getRefname());
break;
case BRANCH_ALREADY_EXISTS:
selectAllAndFocus(nameTxtBox);
msg = Gerrit.M.branchAlreadyExists(error.getRefname());
break;
case BRANCH_CREATION_CONFLICT:
selectAllAndFocus(nameTxtBox);
msg =
Gerrit.M.branchCreationConflict(branchName,
error.getRefname());
break;
default:
msg =
Gerrit.M.branchCreationFailed(branchName,
error.toString());
}
new ErrorDialog(msg).center();
}
}
ProjectApi.createBranch(getProjectKey(), branchName, rev,
new GerritCallback<BranchInfo>() {
@Override
public void onFailure(final Throwable caught) {
public void onSuccess(BranchInfo result) {
addBranch.setEnabled(true);
super.onFailure(caught);
nameTxtBox.setText("");
irevTxtBox.setText("");
Util.PROJECT_SVC.listBranches(getProjectKey(),
new GerritCallback<ListBranchesResult>() {
@Override
public void onSuccess(ListBranchesResult result) {
display(result.getBranches());
}
});
}
});
@Override
public void onFailure(Throwable caught) {
addBranch.setEnabled(true);
selectAllAndFocus(nameTxtBox);
new ErrorDialog(caught.getMessage()).center();
}
});
}
private static void selectAllAndFocus(final TextBox textBox) {

View File

@ -0,0 +1,26 @@
// 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.client.projects;
import com.google.gwt.core.client.JavaScriptObject;
public class BranchInfo extends JavaScriptObject {
public final native String ref() /*-{ return this.ref; }-*/;
public final native String revision() /*-{ return this.revision; }-*/;
public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
protected BranchInfo() {
}
}

View File

@ -33,6 +33,15 @@ public class ProjectApi {
.put(input, asyncCallback);
}
/** Create a new branch */
public static void createBranch(Project.NameKey projectName, String ref,
String revision, AsyncCallback<BranchInfo> asyncCallback) {
BranchInput input = BranchInput.create();
input.setRevision(revision);
new RestApi("/projects/").id(projectName.get()).view("branches").id(ref)
.ifNoneMatch().put(input, asyncCallback);
}
static RestApi config(Project.NameKey name) {
return new RestApi("/projects/").id(name.get()).view("config");
}
@ -53,4 +62,15 @@ public class ProjectApi {
final native void setCreateEmptyCommit(boolean cc) /*-{ if(cc)this.create_empty_commit=cc; }-*/;
}
private static class BranchInput extends JavaScriptObject {
static BranchInput create() {
return (BranchInput) createObject();
}
protected BranchInput() {
}
final native void setRevision(String r) /*-{ if(r)this.revision=r; }-*/;
}
}

View File

@ -15,7 +15,6 @@
package com.google.gerrit.httpd.rpc.project;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.AddBranchResult;
import com.google.gerrit.common.data.ListBranchesResult;
import com.google.gerrit.common.data.ProjectAccess;
import com.google.gerrit.common.data.ProjectAdminService;
@ -32,7 +31,6 @@ import java.util.List;
import java.util.Set;
class ProjectAdminServiceImpl implements ProjectAdminService {
private final AddBranch.Factory addBranchFactory;
private final ChangeProjectAccess.Factory changeProjectAccessFactory;
private final ReviewProjectAccess.Factory reviewProjectAccessFactory;
private final ChangeProjectSettings.Factory changeProjectSettingsFactory;
@ -43,8 +41,7 @@ class ProjectAdminServiceImpl implements ProjectAdminService {
private final ProjectDetailFactory.Factory projectDetailFactory;
@Inject
ProjectAdminServiceImpl(final AddBranch.Factory addBranchFactory,
final ChangeProjectAccess.Factory changeProjectAccessFactory,
ProjectAdminServiceImpl(final ChangeProjectAccess.Factory changeProjectAccessFactory,
final ReviewProjectAccess.Factory reviewProjectAccessFactory,
final ChangeProjectSettings.Factory changeProjectSettingsFactory,
final DeleteBranches.Factory deleteBranchesFactory,
@ -52,7 +49,6 @@ class ProjectAdminServiceImpl implements ProjectAdminService {
final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
final ProjectAccessFactory.Factory projectAccessFactory,
final ProjectDetailFactory.Factory projectDetailFactory) {
this.addBranchFactory = addBranchFactory;
this.changeProjectAccessFactory = changeProjectAccessFactory;
this.reviewProjectAccessFactory = reviewProjectAccessFactory;
this.changeProjectSettingsFactory = changeProjectSettingsFactory;
@ -119,12 +115,4 @@ class ProjectAdminServiceImpl implements ProjectAdminService {
final AsyncCallback<Set<Branch.NameKey>> callback) {
deleteBranchesFactory.create(projectName, toRemove).to(callback);
}
@Override
public void addBranch(final Project.NameKey projectName,
final String branchName, final String startingRevision,
final AsyncCallback<AddBranchResult> callback) {
addBranchFactory.create(projectName, branchName, startingRevision).to(
callback);
}
}

View File

@ -28,7 +28,6 @@ public class ProjectModule extends RpcServletModule {
install(new FactoryModule() {
@Override
protected void configure() {
factory(AddBranch.Factory.class);
factory(ChangeProjectAccess.Factory.class);
factory(ReviewProjectAccess.Factory.class);
factory(ChangeProjectSettings.Factory.class);

View File

@ -15,6 +15,7 @@
package com.google.gerrit.server.project;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.ChildCollection;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@ -29,15 +30,18 @@ import java.io.IOException;
import java.util.List;
public class BranchesCollection implements
ChildCollection<ProjectResource, BranchResource> {
ChildCollection<ProjectResource, BranchResource>,
AcceptsCreate<ProjectResource> {
private final DynamicMap<RestView<BranchResource>> views;
private final Provider<ListBranches> list;
private final CreateBranch.Factory createBranchFactory;
@Inject
BranchesCollection(DynamicMap<RestView<BranchResource>> views,
Provider<ListBranches> list) {
Provider<ListBranches> list, CreateBranch.Factory createBranchFactory) {
this.views = views;
this.list = list;
this.createBranchFactory = createBranchFactory;
}
@Override
@ -66,4 +70,10 @@ public class BranchesCollection implements
public DynamicMap<RestView<BranchResource>> views() {
return views;
}
@SuppressWarnings("unchecked")
@Override
public CreateBranch create(ProjectResource parent, IdString name) {
return createBranchFactory.create(name.get());
}
}

View File

@ -1,4 +1,4 @@
// Copyright (C) 2009 The Android Open Source Project
// 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.
@ -12,26 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd.rpc.project;
package com.google.gerrit.server.project;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.AddBranchResult;
import com.google.gerrit.common.errors.InvalidRevisionException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
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.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.project.CreateBranch.Input;
import com.google.gerrit.server.project.ListBranches.BranchInfo;
import com.google.gerrit.server.util.MagicBranch;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@ -45,101 +47,92 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
class AddBranch extends Handler<AddBranchResult> {
private static final Logger log = LoggerFactory.getLogger(AddBranch.class);
public class CreateBranch implements RestModifyView<ProjectResource, Input> {
private static final Logger log = LoggerFactory.getLogger(CreateBranch.class);
interface Factory {
AddBranch create(@Assisted Project.NameKey projectName,
@Assisted("branchName") String branchName,
@Assisted("startingRevision") String startingRevision);
static class Input {
String ref;
@DefaultInput
String revision;
}
static interface Factory {
CreateBranch create(String ref);
}
private final ProjectControl.Factory projectControlFactory;
private final ListBranches.Factory listBranchesFactory;
private final IdentifiedUser identifiedUser;
private final GitRepositoryManager repoManager;
private final GitReferenceUpdated referenceUpdated;
private final ChangeHooks hooks;
private final Project.NameKey projectName;
private final String branchName;
private final String startingRevision;
private String ref;
@Inject
AddBranch(final ProjectControl.Factory projectControlFactory,
final ListBranches.Factory listBranchesFactory,
final IdentifiedUser identifiedUser,
final GitRepositoryManager repoManager,
GitReferenceUpdated referenceUpdated,
final ChangeHooks hooks,
@Assisted Project.NameKey projectName,
@Assisted("branchName") String branchName,
@Assisted("startingRevision") String startingRevision) {
this.projectControlFactory = projectControlFactory;
this.listBranchesFactory = listBranchesFactory;
CreateBranch(IdentifiedUser identifiedUser, GitRepositoryManager repoManager,
GitReferenceUpdated referenceUpdated, ChangeHooks hooks,
@Assisted String ref) {
this.identifiedUser = identifiedUser;
this.repoManager = repoManager;
this.referenceUpdated = referenceUpdated;
this.hooks = hooks;
this.projectName = projectName;
this.branchName = branchName;
this.startingRevision = startingRevision;
this.ref = ref;
}
@Override
public AddBranchResult call() throws NoSuchProjectException, IOException {
final ProjectControl projectControl =
projectControlFactory.controlFor(projectName);
String refname = branchName;
while (refname.startsWith("/")) {
refname = refname.substring(1);
public BranchInfo apply(ProjectResource rsrc, Input input)
throws BadRequestException, ResourceConflictException, IOException {
if (input == null) {
input = new Input();
}
if (!refname.startsWith(Constants.R_REFS)) {
refname = Constants.R_HEADS + refname;
if (input.ref != null && !ref.equals(input.ref)) {
throw new BadRequestException("ref must match URL");
}
if (!Repository.isValidRefName(refname)) {
return new AddBranchResult(new AddBranchResult.Error(
AddBranchResult.Error.Type.INVALID_NAME, refname));
if (input.revision == null) {
input.revision = Constants.HEAD;
}
if (MagicBranch.isMagicBranch(refname)) {
return new AddBranchResult(
new AddBranchResult.Error(
AddBranchResult.Error.Type.BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX,
MagicBranch.getMagicRefNamePrefix(refname)));
while (ref.startsWith("/")) {
ref = ref.substring(1);
}
if (!ref.startsWith(Constants.R_REFS)) {
ref = Constants.R_HEADS + ref;
}
if (!Repository.isValidRefName(ref)) {
throw new BadRequestException("invalid branch name \"" + ref + "\"");
}
if (MagicBranch.isMagicBranch(ref)) {
throw new BadRequestException("not allowed to create branches under \""
+ MagicBranch.getMagicRefNamePrefix(ref) + "\"");
}
final Branch.NameKey name = new Branch.NameKey(projectName, refname);
final RefControl refControl = projectControl.controlForRef(name);
final Repository repo = repoManager.openRepository(projectName);
final Branch.NameKey name = new Branch.NameKey(rsrc.getNameKey(), ref);
final RefControl refControl = rsrc.getControl().controlForRef(name);
final Repository repo = repoManager.openRepository(rsrc.getNameKey());
try {
final ObjectId revid = parseStartingRevision(repo);
final ObjectId revid = parseBaseRevision(repo, rsrc.getNameKey(), input.revision);
final RevWalk rw = verifyConnected(repo, revid);
RevObject object = rw.parseAny(revid);
if (refname.startsWith(Constants.R_HEADS)) {
if (ref.startsWith(Constants.R_HEADS)) {
// Ensure that what we start the branch from is a commit. If we
// were given a tag, deference to the commit instead.
//
try {
object = rw.parseCommit(object);
} catch (IncorrectObjectTypeException notCommit) {
throw new IllegalStateException(startingRevision + " not a commit");
throw new BadRequestException("\"" + input.revision + "\" not a commit");
}
}
if (!refControl.canCreate(rw, object)) {
throw new IllegalStateException("Cannot create " + refname);
throw new IllegalStateException("Cannot create \"" + ref + "\"");
}
try {
final RefUpdate u = repo.updateRef(refname);
final RefUpdate u = repo.updateRef(ref);
u.setExpectedOldObjectId(ObjectId.zeroId());
u.setNewObjectId(object.copy());
u.setRefLogIdent(identifiedUser.newRefLogIdent());
u.setRefLogMessage("created via web from " + startingRevision, false);
u.setRefLogMessage("created via REST from " + input.revision, false);
final RefUpdate.Result result = u.update(rw);
switch (result) {
case FAST_FORWARD:
@ -149,15 +142,16 @@ class AddBranch extends Handler<AddBranchResult> {
hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
break;
case LOCK_FAILURE:
if (repo.getRef(refname) != null) {
return new AddBranchResult(new AddBranchResult.Error(
AddBranchResult.Error.Type.BRANCH_ALREADY_EXISTS, refname));
if (repo.getRef(ref) != null) {
throw new ResourceConflictException("branch \"" + ref
+ "\" already exists");
}
String refPrefix = getRefPrefix(refname);
String refPrefix = getRefPrefix(ref);
while (!Constants.R_HEADS.equals(refPrefix)) {
if (repo.getRef(refPrefix) != null) {
return new AddBranchResult(new AddBranchResult.Error(
AddBranchResult.Error.Type.BRANCH_CREATION_CONFLICT, refPrefix));
throw new ResourceConflictException("Cannot create branch \""
+ ref + "\" since it conflicts with branch \"" + refPrefix
+ "\".");
}
refPrefix = getRefPrefix(refPrefix);
}
@ -165,18 +159,21 @@ class AddBranch extends Handler<AddBranchResult> {
throw new IOException(result.name());
}
}
BranchInfo b = new BranchInfo();
b.ref = ref;
b.revision = revid.getName();
b.setCanDelete(refControl.canDelete());
return b;
} catch (IOException err) {
log.error("Cannot create branch " + name, err);
log.error("Cannot create branch \"" + name + "\"", err);
throw err;
}
} catch (InvalidRevisionException e) {
return new AddBranchResult(new AddBranchResult.Error(
AddBranchResult.Error.Type.INVALID_REVISION));
throw new BadRequestException("invalid revision \"" + input.revision + "\"");
} finally {
repo.close();
}
return new AddBranchResult(listBranchesFactory.create(projectName).call());
}
private static String getRefPrefix(final String refName) {
@ -187,17 +184,21 @@ class AddBranch extends Handler<AddBranchResult> {
return Constants.R_HEADS;
}
private ObjectId parseStartingRevision(final Repository repo)
private ObjectId parseBaseRevision(Repository repo,
Project.NameKey projectName, String baseRevision)
throws InvalidRevisionException {
try {
final ObjectId revid = repo.resolve(startingRevision);
final ObjectId revid = repo.resolve(baseRevision);
if (revid == null) {
throw new InvalidRevisionException();
}
return revid;
} catch (IOException err) {
log.error("Cannot resolve \"" + startingRevision + "\" in project \""
+ projectName + "\"", err);
log.error("Cannot resolve \"" + baseRevision + "\" in project \""
+ projectName.get() + "\"", err);
throw new InvalidRevisionException();
} catch (RevisionSyntaxException err) {
log.error("Invalid revision syntax \"" + baseRevision + "\"", err);
throw new InvalidRevisionException();
}
}

View File

@ -53,7 +53,9 @@ public class Module extends RestApiModule {
post(PROJECT_KIND, "gc").to(GarbageCollect.class);
child(PROJECT_KIND, "branches").to(BranchesCollection.class);
put(BRANCH_KIND).to(PutBranch.class);
get(BRANCH_KIND).to(GetBranch.class);
install(new FactoryModuleBuilder().build(CreateBranch.Factory.class));
child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
get(DASHBOARD_KIND).to(GetDashboard.class);

View File

@ -0,0 +1,29 @@
// 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.project;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.project.CreateBranch.Input;
public class PutBranch implements RestModifyView<BranchResource, Input> {
@Override
public Object apply(BranchResource rsrc, Input input)
throws ResourceConflictException {
throw new ResourceConflictException("Branch \"" + rsrc.getBranchInfo().ref
+ "\" already exists");
}
}