Add a Branches tab to the project admin screen

This tab allows project owners to view branches, delete branches,
and create new branches from any Git commit SHA-1 expression we
can recognize in JGit (which is most of the expressions, except
reflog queries).

Bug: GERRIT-20
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2009-02-10 20:07:56 -08:00
parent 99bab581a0
commit 3a8874b34e
8 changed files with 602 additions and 0 deletions

View File

@@ -19,6 +19,8 @@ import com.google.gwt.i18n.client.Constants;
public interface AdminConstants extends Constants {
String defaultAccountName();
String defaultAccountGroupName();
String defaultBranchName();
String defaultRevisionSpec();
String buttonDeleteGroupMembers();
String buttonAddGroupMember();
@@ -43,8 +45,14 @@ public interface AdminConstants extends Constants {
String columnApprovalCategory();
String columnRightRange();
String columnBranchName();
String initialRevision();
String buttonAddBranch();
String buttonDeleteBranch();
String groupListTitle();
String projectListTitle();
String projectAdminTabGeneral();
String projectAdminTabBranches();
String projectAdminTabAccess();
}

View File

@@ -1,5 +1,7 @@
defaultAccountName = Name or Email
defaultAccountGroupName = Group Name
defaultBranchName = Branch Name
defaultRevisionSpec = Revision (Branch or SHA-1)
buttonDeleteGroupMembers = Delete
buttonAddGroupMember = Add
@@ -24,7 +26,13 @@ columnProjectDescription = Description
columnApprovalCategory = Category
columnRightRange = Permitted Range
columnBranchName = Branch Name
initialRevision = Initial Revision
buttonAddBranch = Create Branch
buttonDeleteBranch = Delete
groupListTitle = Groups
projectListTitle = Projects
projectAdminTabGeneral = General
projectAdminTabBranches = Branches
projectAdminTabAccess = Access

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.client.admin;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.Link;
import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.AccountScreen;
import com.google.gerrit.client.ui.LazyTabChild;
@@ -29,6 +30,7 @@ import java.util.List;
public class ProjectAdminScreen extends AccountScreen {
static final String INFO_TAB = "info";
static final String BRANCH_TAB = "branches";
static final String ACCESS_TAB = "access";
private String initialTabToken;
@@ -73,6 +75,16 @@ public class ProjectAdminScreen extends AccountScreen {
}, Util.C.projectAdminTabGeneral());
tabTokens.add(Link.toProjectAdmin(projectId, INFO_TAB));
if (!ProjectRight.WILD_PROJECT.equals(projectId)) {
tabs.add(new LazyTabChild<ProjectBranchesPanel>() {
@Override
protected ProjectBranchesPanel create() {
return new ProjectBranchesPanel(projectId);
}
}, Util.C.projectAdminTabBranches());
tabTokens.add(Link.toProjectAdmin(projectId, BRANCH_TAB));
}
tabs.add(new LazyTabChild<ProjectRightsPanel>() {
@Override
protected ProjectRightsPanel create() {

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.client.admin;
import com.google.gerrit.client.reviewdb.ApprovalCategory;
import com.google.gerrit.client.reviewdb.Branch;
import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.rpc.SignInRequired;
@@ -47,4 +48,15 @@ public interface ProjectAdminService extends RemoteJsonService {
void addRight(Project.Id projectId, ApprovalCategory.Id categoryId,
String groupName, short min, short max,
AsyncCallback<ProjectDetail> callback);
@SignInRequired
void listBranches(Project.Id project, AsyncCallback<List<Branch>> callback);
@SignInRequired
void addBranch(Project.Id project, String branchName,
String startingRevision, AsyncCallback<List<Branch>> callback);
@SignInRequired
void deleteBranch(Set<Branch.NameKey> ids,
AsyncCallback<Set<Branch.NameKey>> callback);
}

View File

@@ -0,0 +1,316 @@
// Copyright 2009 Google Inc.
//
// 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.admin;
import com.google.gerrit.client.data.GitwebLink;
import com.google.gerrit.client.reviewdb.Branch;
import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.rpc.Common;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.InvalidNameException;
import com.google.gerrit.client.rpc.InvalidRevisionException;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusListenerAdapter;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.SourcesTableEvents;
import com.google.gwt.user.client.ui.TableListener;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwtjsonrpc.client.RemoteJsonException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ProjectBranchesPanel extends Composite {
private Project.Id projectId;
private BranchesTable branches;
private Button delBranch;
private Button addBranch;
private TextBox nameTxtBox;
private TextBox irevTxtBox;
public ProjectBranchesPanel(final Project.Id toShow) {
final FlowPanel body = new FlowPanel();
initBranches(body);
initWidget(body);
projectId = toShow;
}
@Override
public void onLoad() {
enableForm(false);
super.onLoad();
Util.PROJECT_SVC.listBranches(projectId,
new GerritCallback<List<Branch>>() {
public void onSuccess(final List<Branch> result) {
enableForm(true);
branches.display(result);
branches.finishDisplay(true);
}
});
}
private void enableForm(final boolean on) {
delBranch.setEnabled(on);
addBranch.setEnabled(on);
nameTxtBox.setEnabled(on);
irevTxtBox.setEnabled(on);
}
private void initBranches(final Panel body) {
final FlowPanel addPanel = new FlowPanel();
addPanel.setStyleName("gerrit-AddSshKeyPanel");
final Grid addGrid = new Grid(2, 2);
nameTxtBox = new TextBox();
nameTxtBox.setVisibleLength(50);
nameTxtBox.setText(Util.C.defaultBranchName());
nameTxtBox.addStyleName("gerrit-InputFieldTypeHint");
nameTxtBox.addFocusListener(new FocusListenerAdapter() {
@Override
public void onFocus(Widget sender) {
if (Util.C.defaultBranchName().equals(nameTxtBox.getText())) {
nameTxtBox.setText("");
nameTxtBox.removeStyleName("gerrit-InputFieldTypeHint");
}
}
@Override
public void onLostFocus(Widget sender) {
if ("".equals(nameTxtBox.getText())) {
nameTxtBox.setText(Util.C.defaultBranchName());
nameTxtBox.addStyleName("gerrit-InputFieldTypeHint");
}
}
});
addGrid.setText(0, 0, Util.C.columnBranchName() + ":");
addGrid.setWidget(0, 1, nameTxtBox);
irevTxtBox = new TextBox();
irevTxtBox.setVisibleLength(50);
irevTxtBox.setText(Util.C.defaultRevisionSpec());
irevTxtBox.addStyleName("gerrit-InputFieldTypeHint");
irevTxtBox.addFocusListener(new FocusListenerAdapter() {
@Override
public void onFocus(Widget sender) {
if (Util.C.defaultRevisionSpec().equals(irevTxtBox.getText())) {
irevTxtBox.setText("");
irevTxtBox.removeStyleName("gerrit-InputFieldTypeHint");
}
}
@Override
public void onLostFocus(Widget sender) {
if ("".equals(irevTxtBox.getText())) {
irevTxtBox.setText(Util.C.defaultRevisionSpec());
irevTxtBox.addStyleName("gerrit-InputFieldTypeHint");
}
}
});
addGrid.setText(1, 0, Util.C.initialRevision() + ":");
addGrid.setWidget(1, 1, irevTxtBox);
addBranch = new Button(Util.C.buttonAddBranch());
addBranch.addClickListener(new ClickListener() {
public void onClick(final Widget sender) {
doAddNewBranch();
}
});
addPanel.add(addGrid);
addPanel.add(addBranch);
branches = new BranchesTable();
delBranch = new Button(Util.C.buttonDeleteBranch());
delBranch.addClickListener(new ClickListener() {
public void onClick(final Widget sender) {
branches.deleteChecked();
}
});
body.add(branches);
body.add(delBranch);
body.add(addPanel);
}
private void doAddNewBranch() {
String branchName = nameTxtBox.getText();
if ("".equals(branchName) || Util.C.defaultBranchName().equals(branchName)) {
return;
}
String rev = irevTxtBox.getText();
if ("".equals(rev) || Util.C.defaultRevisionSpec().equals(rev)) {
return;
}
if (!branchName.startsWith(Branch.R_REFS)) {
branchName = Branch.R_HEADS + branchName;
}
addBranch.setEnabled(false);
Util.PROJECT_SVC.addBranch(projectId, branchName, rev,
new GerritCallback<List<Branch>>() {
public void onSuccess(final List<Branch> result) {
addBranch.setEnabled(true);
nameTxtBox.setText("");
irevTxtBox.setText("");
branches.display(result);
}
@Override
public void onFailure(final Throwable caught) {
if (caught instanceof InvalidNameException
|| caught instanceof RemoteJsonException
&& caught.getMessage().equals(InvalidNameException.MESSAGE)) {
nameTxtBox.selectAll();
nameTxtBox.setFocus(true);
} else if (caught instanceof InvalidRevisionException
|| caught instanceof RemoteJsonException
&& caught.getMessage().equals(InvalidRevisionException.MESSAGE)) {
irevTxtBox.selectAll();
irevTxtBox.setFocus(true);
}
addBranch.setEnabled(true);
super.onFailure(caught);
}
});
}
private class BranchesTable extends FancyFlexTable<Branch> {
BranchesTable() {
table.setText(0, 2, Util.C.columnBranchName());
table.setHTML(0, 3, "&nbsp;");
table.addTableListener(new TableListener() {
public void onCellClicked(SourcesTableEvents sender, int row, int cell) {
if (cell != 1 && getRowItem(row) != null) {
movePointerTo(row);
}
}
});
final FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(0, 1, S_ICON_HEADER);
fmt.addStyleName(0, 2, S_DATA_HEADER);
fmt.addStyleName(0, 3, S_DATA_HEADER);
}
@Override
protected Object getRowItemKey(final Branch item) {
return item.getId();
}
@Override
protected boolean onKeyPress(final char keyCode, final int modifiers) {
if (super.onKeyPress(keyCode, modifiers)) {
return true;
}
if (modifiers == 0) {
switch (keyCode) {
case 's':
case 'c':
toggleCurrentRow();
return true;
}
}
return false;
}
@Override
protected void onOpenItem(final Branch item) {
toggleCurrentRow();
}
private void toggleCurrentRow() {
final CheckBox cb = (CheckBox) table.getWidget(getCurrentRow(), 1);
cb.setChecked(!cb.isChecked());
}
void deleteChecked() {
final HashSet<Branch.NameKey> ids = new HashSet<Branch.NameKey>();
for (int row = 1; row < table.getRowCount(); row++) {
final Branch k = getRowItem(row);
if (k != null && table.getWidget(row, 1) instanceof CheckBox
&& ((CheckBox) table.getWidget(row, 1)).isChecked()) {
ids.add(k.getNameKey());
}
}
if (ids.isEmpty()) {
return;
}
Util.PROJECT_SVC.deleteBranch(ids,
new GerritCallback<Set<Branch.NameKey>>() {
public void onSuccess(final Set<Branch.NameKey> deleted) {
for (int row = 1; row < table.getRowCount();) {
final Branch k = getRowItem(row);
if (k != null && deleted.contains(k.getNameKey())) {
table.removeRow(row);
} else {
row++;
}
}
}
});
}
void display(final List<Branch> result) {
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
for (final Branch k : result) {
final int row = table.getRowCount();
table.insertRow(row);
applyDataRowStyle(row);
populate(row, k);
}
}
void populate(final int row, final Branch k) {
final GitwebLink c = Common.getGerritConfig().getGitwebLink();
table.setWidget(row, 1, new CheckBox());
table.setText(row, 2, k.getShortName());
if (c != null) {
table.setWidget(row, 3, new Anchor("(gitweb)", false, c.toBranch(k
.getNameKey())));
} else {
table.setHTML(row, 3, "&nbsp;");
}
final FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(row, 1, S_ICON_CELL);
fmt.addStyleName(row, 2, S_DATA_CELL);
fmt.addStyleName(row, 3, S_DATA_CELL);
setRowItem(row, k);
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2009 Google Inc.
//
// 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.rpc;
/** Error indicating the entity name is invalid as supplied. */
public class InvalidNameException extends Exception {
public static final String MESSAGE = "Invalid Name";
public InvalidNameException() {
super(MESSAGE);
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2009 Google Inc.
//
// 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.rpc;
/** Error indicating the revision is invalid as supplied. */
public class InvalidRevisionException extends Exception {
public static final String MESSAGE = "Invalid Revision";
public InvalidRevisionException() {
super(MESSAGE);
}
}

View File

@@ -21,11 +21,14 @@ import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.ApprovalCategory;
import com.google.gerrit.client.reviewdb.ApprovalCategoryValue;
import com.google.gerrit.client.reviewdb.Branch;
import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gerrit.client.rpc.BaseServiceImplementation;
import com.google.gerrit.client.rpc.Common;
import com.google.gerrit.client.rpc.InvalidNameException;
import com.google.gerrit.client.rpc.InvalidRevisionException;
import com.google.gerrit.client.rpc.NoSuchEntityException;
import com.google.gerrit.git.InvalidRepositoryException;
import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -34,9 +37,16 @@ import com.google.gwtorm.client.OrmException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spearce.jgit.errors.IncorrectObjectTypeException;
import org.spearce.jgit.errors.MissingObjectException;
import org.spearce.jgit.lib.Constants;
import org.spearce.jgit.lib.LockFile;
import org.spearce.jgit.lib.ObjectId;
import org.spearce.jgit.lib.Ref;
import org.spearce.jgit.lib.RefUpdate;
import org.spearce.jgit.lib.Repository;
import org.spearce.jgit.revwalk.ObjectWalk;
import org.spearce.jgit.revwalk.RevCommit;
import java.io.File;
import java.io.IOException;
@@ -250,6 +260,194 @@ public class ProjectAdminServiceImpl extends BaseServiceImplementation
});
}
public void listBranches(final Project.Id project,
final AsyncCallback<List<Branch>> callback) {
run(callback, new Action<List<Branch>>() {
public List<Branch> run(ReviewDb db) throws OrmException, Failure {
final ProjectCache.Entry e = Common.getProjectCache().get(project);
if (e == null) {
throw new Failure(new NoSuchEntityException());
}
assertCanRead(e.getProject().getNameKey());
return db.branches().byProject(e.getProject().getNameKey()).toList();
}
});
}
public void deleteBranch(final Set<Branch.NameKey> ids,
final AsyncCallback<Set<Branch.NameKey>> callback) {
run(callback, new Action<Set<Branch.NameKey>>() {
public Set<Branch.NameKey> run(ReviewDb db) throws OrmException, Failure {
final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>();
final Set<Project.Id> owned = ids(myOwnedProjects(db));
Boolean amAdmin = null;
for (final Branch.NameKey k : ids) {
final ProjectCache.Entry e;
e = Common.getProjectCache().get(k.getParentKey());
if (e == null) {
throw new Failure(new NoSuchEntityException());
}
if (!owned.contains(e.getProject().getId())) {
if (amAdmin == null) {
amAdmin =
Common.getGroupCache().isAdministrator(Common.getAccountId());
}
if (!amAdmin) {
throw new Failure(new NoSuchEntityException());
}
}
}
for (final Branch.NameKey k : ids) {
final Branch m = db.branches().get(k);
if (m == null) {
continue;
}
final Repository r;
try {
r = server.getRepositoryCache().get(k.getParentKey().get());
} catch (InvalidRepositoryException e) {
throw new Failure(new NoSuchEntityException());
}
final RefUpdate.Result result;
try {
final RefUpdate u = r.updateRef(m.getName());
u.setForceUpdate(true);
result = u.delete();
} catch (IOException e) {
log.error("Cannot delete " + k, e);
continue;
}
switch (result) {
case NEW:
case NO_CHANGE:
case FAST_FORWARD:
case FORCED:
db.branches().delete(Collections.singleton(m));
deleted.add(m.getNameKey());
break;
case REJECTED_CURRENT_BRANCH:
log.warn("Cannot delete " + k + ": " + result.name());
break;
default:
log.error("Cannot delete " + k + ": " + result.name());
break;
}
}
return deleted;
}
});
}
public void addBranch(final Project.Id projectId, final String branchName,
final String startingRevision, final AsyncCallback<List<Branch>> callback) {
run(callback, new Action<List<Branch>>() {
public List<Branch> run(ReviewDb db) throws OrmException, Failure {
String refname = branchName;
if (!refname.startsWith(Constants.R_REFS)) {
refname = Constants.R_HEADS + refname;
}
if (!Repository.isValidRefName(refname)) {
throw new Failure(new InvalidNameException());
}
final Account me = Common.getAccountCache().get(Common.getAccountId());
if (me == null) {
throw new Failure(new NoSuchEntityException());
}
final ProjectCache.Entry pce = Common.getProjectCache().get(projectId);
if (pce == null) {
throw new Failure(new NoSuchEntityException());
}
assertAmProjectOwner(db, projectId);
final String repoName = pce.getProject().getName();
final Repository repo;
try {
repo = server.getRepositoryCache().get(repoName);
} catch (InvalidRepositoryException e1) {
throw new Failure(new NoSuchEntityException());
}
// Convert the name given by the user into a valid object.
//
final ObjectId revid;
try {
revid = repo.resolve(startingRevision);
if (revid == null) {
throw new Failure(new InvalidRevisionException());
}
} catch (IOException err) {
log.error("Cannot resolve \"" + startingRevision + "\" in "
+ repoName, err);
throw new Failure(new InvalidRevisionException());
}
// Ensure it is fully connected in this repository. If not,
// we can't safely create a ref to it as objects are missing
//
final RevCommit revcommit;
final ObjectWalk rw = new ObjectWalk(repo);
try {
try {
revcommit = rw.parseCommit(revid);
rw.markStart(revcommit);
} catch (IncorrectObjectTypeException err) {
throw new Failure(new InvalidRevisionException());
}
for (final Ref r : repo.getAllRefs().values()) {
try {
rw.markUninteresting(rw.parseAny(r.getObjectId()));
} catch (MissingObjectException err) {
continue;
}
}
rw.checkConnectivity();
} catch (IncorrectObjectTypeException err) {
throw new Failure(new InvalidRevisionException());
} catch (MissingObjectException err) {
throw new Failure(new InvalidRevisionException());
} catch (IOException err) {
log.error("Repository " + repoName + " possibly corrupt", err);
throw new Failure(new InvalidRevisionException());
}
final Branch.NameKey name =
new Branch.NameKey(pce.getProject().getNameKey(), refname);
try {
final RefUpdate u = repo.updateRef(refname);
u.setExpectedOldObjectId(ObjectId.zeroId());
u.setNewObjectId(revid);
u.setRefLogIdent(ChangeUtil.toPersonIdent(me));
u.setRefLogMessage("created via web", true);
final RefUpdate.Result result = u.update(rw);
switch (result) {
case FAST_FORWARD:
case NEW:
case NO_CHANGE:
break;
default:
log.error("Cannot create branch " + name + ": " + result.name());
throw new Failure(new IOException(result.name()));
}
} catch (IOException err) {
log.error("Cannot create branch " + name, err);
throw new Failure(err);
}
final Branch.Id id = new Branch.Id(db.nextBranchId());
final Branch newBranch = new Branch(name, id);
db.branches().insert(Collections.singleton(newBranch));
return db.branches().byProject(pce.getProject().getNameKey()).toList();
}
});
}
private void assertAmProjectOwner(final ReviewDb db,
final Project.Id projectId) throws Failure {
final ProjectCache.Entry p = Common.getProjectCache().get(projectId);