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:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -80,6 +80,7 @@ public interface ChangeConstants extends Constants {
 | 
			
		||||
  String changeInfoBlockOwner();
 | 
			
		||||
  String changeInfoBlockProject();
 | 
			
		||||
  String changeInfoBlockBranch();
 | 
			
		||||
  String changeInfoBlockTopic();
 | 
			
		||||
  String changeInfoBlockUploaded();
 | 
			
		||||
  String changeInfoBlockUpdated();
 | 
			
		||||
  String changeInfoBlockStatus();
 | 
			
		||||
 
 | 
			
		||||
@@ -57,6 +57,7 @@ approvalTableAddReviewer = Add Reviewer
 | 
			
		||||
changeInfoBlockOwner = Owner
 | 
			
		||||
changeInfoBlockProject = Project
 | 
			
		||||
changeInfoBlockBranch = Branch
 | 
			
		||||
changeInfoBlockTopic = Topic
 | 
			
		||||
changeInfoBlockUploaded = Uploaded
 | 
			
		||||
changeInfoBlockUpdated = Updated
 | 
			
		||||
changeInfoBlockStatus = Status
 | 
			
		||||
 
 | 
			
		||||
@@ -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()));
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user