Support topic branch tags on changes

The topic tag can be attached to a change during upload by suffixing
the 'refs/for/branch_name' with the topic label, treating the branch
as though it were a directory.

For example, to tag 'exp/nosql' onto a change headed for the
'sandbox/future-stuff' branch, git push can be used as:

  git push URL HEAD:refs/for/sandbox/future-stuff/exp-nosql

If the topic is supplied, it is displayed in the branch column of
a change, in parens.  If no topic was set, only the branch is shown.

Bug: issue 51
Change-Id: I07d6c137fc9aefa8c1ee1652bf1e7bcde9d33674
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2010-07-15 12:24:11 -07:00
parent 72fc9bb5a8
commit d50c94e759
11 changed files with 129 additions and 40 deletions

View File

@@ -64,6 +64,11 @@ Send a review for a change on the master branch to charlie@example.com:
git push --receive-pack='git receive-pack --reviewer charlie@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master
=====
Send reviews, but tagging them with the topic name 'bug42':
=====
git push --receive-pack='git receive-pack --reviewer charlie@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master/bug42
=====
Also CC two other parties:
=====
git push --receive-pack='git receive-pack --reviewer charlie@example.com --cc alice@example.com --cc bob@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master

View File

@@ -116,6 +116,16 @@ Other users (e.g. project owners) who have configured Gerrit to
notify them of new changes will be automatically sent an email
message when the push is completed.
To include a short tag associated with all of the changes in the
same group, such as the local topic branch name, append it after
the destination branch name. In this example the short topic tag
'driver/i42' will be saved on each change this push creates or
updates:
====
git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental/driver/i42
====
If you are frequently uploading changes to the same Gerrit server,
consider adding an SSH host block in `~/.ssh/config` to remember
your username, hostname and port number. This permits the use of

View File

@@ -27,6 +27,7 @@ public class ChangeInfo {
protected Change.Status status;
protected ProjectInfo project;
protected String branch;
protected String topic;
protected boolean starred;
protected Timestamp lastUpdatedOn;
protected String sortKey;
@@ -42,6 +43,7 @@ public class ChangeInfo {
status = c.getStatus();
project = new ProjectInfo(c.getProject());
branch = c.getDest().getShortName();
topic = c.getTopic();
lastUpdatedOn = c.getLastUpdatedOn();
sortKey = c.getSortKey();
}
@@ -74,6 +76,10 @@ public class ChangeInfo {
return branch;
}
public String getTopic() {
return topic;
}
public boolean isStarred() {
return starred;
}

View File

@@ -80,6 +80,7 @@ public interface ChangeConstants extends Constants {
String changeInfoBlockOwner();
String changeInfoBlockProject();
String changeInfoBlockBranch();
String changeInfoBlockTopic();
String changeInfoBlockUploaded();
String changeInfoBlockUpdated();
String changeInfoBlockStatus();

View File

@@ -57,6 +57,7 @@ approvalTableAddReviewer = Add Reviewer
changeInfoBlockOwner = Owner
changeInfoBlockProject = Project
changeInfoBlockBranch = Branch
changeInfoBlockTopic = Topic
changeInfoBlockUploaded = Uploaded
changeInfoBlockUpdated = Updated
changeInfoBlockStatus = Status

View File

@@ -23,12 +23,9 @@ import com.google.gerrit.client.ui.ProjectLink;
import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.reviewdb.Branch;
import com.google.gerrit.reviewdb.Change;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.ComplexPanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwtexpui.clippy.client.CopyableLabel;
@@ -37,11 +34,12 @@ public class ChangeInfoBlock extends Composite {
private static final int R_OWNER = 1;
private static final int R_PROJECT = 2;
private static final int R_BRANCH = 3;
private static final int R_UPLOADED = 4;
private static final int R_UPDATED = 5;
private static final int R_STATUS = 6;
private static final int R_PERMALINK = 7;
private static final int R_CNT = 8;
private static final int R_TOPIC = 4;
private static final int R_UPLOADED = 5;
private static final int R_UPDATED = 6;
private static final int R_STATUS = 7;
private static final int R_PERMALINK = 8;
private static final int R_CNT = 9;
private final Grid table;
@@ -54,6 +52,7 @@ public class ChangeInfoBlock extends Composite {
initRow(R_OWNER, Util.C.changeInfoBlockOwner());
initRow(R_PROJECT, Util.C.changeInfoBlockProject());
initRow(R_BRANCH, Util.C.changeInfoBlockBranch());
initRow(R_TOPIC, Util.C.changeInfoBlockTopic());
initRow(R_UPLOADED, Util.C.changeInfoBlockUploaded());
initRow(R_UPDATED, Util.C.changeInfoBlockUpdated());
initRow(R_STATUS, Util.C.changeInfoBlockStatus());
@@ -85,6 +84,7 @@ public class ChangeInfoBlock extends Composite {
table.setWidget(R_OWNER, 1, AccountDashboardLink.link(acc, chg.getOwner()));
table.setWidget(R_PROJECT, 1, new ProjectLink(chg.getProject(), chg.getStatus()));
table.setText(R_BRANCH, 1, dst.getShortName());
table.setText(R_TOPIC, 1, chg.getTopic());
table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn()));
table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn()));
table.setText(R_STATUS, 1, Util.toLongString(chg.getStatus()));

View File

@@ -226,7 +226,12 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
table.setWidget(row, C_OWNER, link(c.getOwner()));
table.setWidget(row, C_PROJECT, new ProjectLink(c.getProject().getKey(), c
.getStatus()));
table.setText(row, C_BRANCH, c.getBranch());
String branchText = c.getBranch();
if (c.getTopic() != null) {
branchText += " (" + c.getTopic() + ")";
}
table.setText(row, C_BRANCH, branchText);
table.setText(row, C_LAST_UPDATE, shortFormat(c.getLastUpdatedOn()));
setRowItem(row, c);
}

View File

@@ -351,6 +351,10 @@ public final class Change {
@Column(id = 13)
protected String subject;
/** Topic name assigned by the user, if any. */
@Column(id = 14, notNull = false)
protected String topic;
protected Change() {
}
@@ -456,4 +460,12 @@ public final class Change {
open = newStatus.isOpen();
status = newStatus.getCode();
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
}

View File

@@ -55,7 +55,6 @@ import com.google.gwtorm.client.OrmException;
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.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -159,6 +158,8 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
private Map<ObjectId, Ref> refsById;
private String destTopicName;
@Inject
ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes,
final AccountResolver accountResolver,
@@ -579,38 +580,55 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
destBranchName = Constants.R_HEADS + destBranchName;
}
if (rp.getAdvertisedRefs().containsKey(destBranchName)) {
// We advertised the branch to the client so we know
// the branch exists. Target this branch for the upload.
//
destBranch = new Branch.NameKey(project.getNameKey(), destBranchName);
} else {
// We didn't advertise the branch, because it doesn't exist yet.
// Allow it anyway if HEAD is a symbolic reference to the name.
//
final String head;
try {
head = repo.getFullBranch();
} catch (IOException e) {
log.error("Cannot read HEAD symref", e);
reject(cmd, "internal error");
return;
}
if (head.equals(destBranchName)) {
destBranch = new Branch.NameKey(project.getNameKey(), destBranchName);
}
}
if (destBranch == null) {
String n = destBranchName;
if (n.startsWith(Constants.R_HEADS))
n = n.substring(Constants.R_HEADS.length());
reject(cmd, "branch " + n + " not found");
final String head;
try {
head = repo.getFullBranch();
} catch (IOException e) {
log.error("Cannot read HEAD symref", e);
reject(cmd, "internal error");
return;
}
// Split the destination branch by branch and topic. The topic
// suffix is entirely optional, so it might not even exist.
//
int split = destBranchName.length();
for (;;) {
String name = destBranchName.substring(0, split);
if (rp.getAdvertisedRefs().containsKey(name)) {
// We advertised the branch to the client so we know
// the branch exists. Target this branch for the upload.
//
break;
} else if (head.equals(name)) {
// We didn't advertise the branch, because it doesn't exist yet.
// Allow it anyway as HEAD is a symbolic reference to the name.
//
break;
}
split = name.lastIndexOf('/', split - 1);
if (split <= Constants.R_HEADS.length()) {
String n = destBranchName;
if (n.startsWith(Constants.R_HEADS))
n = n.substring(Constants.R_HEADS.length());
reject(cmd, "branch " + n + " not found");
return;
}
}
if (split < destBranchName.length()) {
destTopicName = destBranchName.substring(split + 1);
} else {
// We use empty string here to denote the topic wasn't
// supplied, but the caller used the syntax that allows
// for a topic to be given.
//
destTopicName = "";
}
destBranch = new Branch.NameKey(project.getNameKey(), //
destBranchName.substring(0, split));
destBranchCtl = projectControl.controlForRef(destBranch);
if (!destBranchCtl.canUpload()) {
reject(cmd);
@@ -858,6 +876,7 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
final Change change =
new Change(changeKey, new Change.Id(db.nextChangeId()), me, destBranch);
change.setTopic(destTopicName.isEmpty() ? null : destTopicName);
change.nextPatchSetId();
final PatchSet ps = new PatchSet(change.currPatchSetId());
@@ -1158,6 +1177,11 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
@Override
public Change update(Change change) {
if (change.getStatus().isOpen()) {
if (destTopicName != null) {
change.setTopic(destTopicName.isEmpty() //
? null //
: destTopicName);
}
change.setStatus(Change.Status.NEW);
change.setCurrentPatchSet(result.info);
ChangeUtil.updated(change);

View File

@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
private static final Class<? extends SchemaVersion> C = Schema_38.class;
private static final Class<? extends SchemaVersion> C = Schema_39.class;
public static class Module extends AbstractModule {
@Override

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2010 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.schema;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class Schema_39 extends SchemaVersion {
@Inject
Schema_39(Provider<Schema_38> prior) {
super(prior);
}
}