Accept refs/for/ options via git push -o

Push options are a capability in the Git protocol to transport
arbitrary strings (example usage: topic strings) to the server through
command line flags rather than by the existing Gerrit-specific magic
branch "refs/for/master%..." convention.

For example with Git >= 2.10:

  git push -o r=email -o topic=fix-bug42 origin HEAD:refs/for/master

can now be used instead of:

  git push origin HEAD:refs/for/master%r=email,topic=fix-bug42

Change-Id: I44e7214b9342d461b07c4b6dd638970cf5fb622f
Signed-off-by: Dan Wang <dwwang@google.com>
This commit is contained in:
Dan Wang
2016-08-16 16:29:43 -07:00
committed by Jonathan Nieder
parent 8f23c50d6f
commit 22f5360b59
4 changed files with 77 additions and 7 deletions

View File

@@ -151,8 +151,15 @@ public class GitUtil {
public static PushResult pushHead(TestRepository<?> testRepo, String ref, public static PushResult pushHead(TestRepository<?> testRepo, String ref,
boolean pushTags, boolean force) throws GitAPIException { boolean pushTags, boolean force) throws GitAPIException {
return pushHead(testRepo, ref, pushTags, force, null);
}
public static PushResult pushHead(TestRepository<?> testRepo, String ref,
boolean pushTags, boolean force, List<String> pushOptions)
throws GitAPIException {
PushCommand pushCmd = testRepo.git().push(); PushCommand pushCmd = testRepo.git().push();
pushCmd.setForce(force); pushCmd.setForce(force);
pushCmd.setPushOptions(pushOptions);
pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref)); pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref));
if (pushTags) { if (pushTags) {
pushCmd.setPushTags(); pushCmd.setPushTags();

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.acceptance;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.pushHead; import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static org.junit.Assert.assertEquals;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@@ -136,6 +137,7 @@ public class PushOneCommit {
private String changeId; private String changeId;
private Tag tag; private Tag tag;
private boolean force; private boolean force;
private List<String> pushOptions;
private final TestRepository<?>.CommitBuilder commitBuilder; private final TestRepository<?>.CommitBuilder commitBuilder;
@@ -275,8 +277,8 @@ public class PushOneCommit {
} }
tagCommand.call(); tagCommand.call();
} }
return new Result(ref, pushHead(testRepo, ref, tag != null, force), c, return new Result(ref,
subject); pushHead(testRepo, ref, tag != null, force, pushOptions), c, subject);
} }
public void setTag(final Tag tag) { public void setTag(final Tag tag) {
@@ -287,6 +289,14 @@ public class PushOneCommit {
this.force = force; this.force = force;
} }
public List<String> getPushOptions() {
return pushOptions;
}
public void setPushOptions(List<String> pushOptions) {
this.pushOptions = pushOptions;
}
public void noParents() { public void noParents() {
commitBuilder.noParents(); commitBuilder.noParents();
} }
@@ -326,6 +336,10 @@ public class PushOneCommit {
return commit; return commit;
} }
public void assertPushOptions(List<String> pushOptions) {
assertEquals(pushOptions, getPushOptions());
}
public void assertChange(Change.Status expectedStatus, public void assertChange(Change.Status expectedStatus,
String expectedTopic, TestAccount... expectedReviewers) String expectedTopic, TestAccount... expectedReviewers)
throws OrmException, NoSuchChangeException { throws OrmException, NoSuchChangeException {

View File

@@ -175,6 +175,21 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
r.assertChange(Change.Status.NEW, topic); r.assertChange(Change.Status.NEW, topic);
} }
@Test
public void pushForMasterWithTopicOption() throws Exception {
String topicOption = "topic=myTopic";
List<String> pushOptions = new ArrayList<>();
pushOptions.add(topicOption);
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
push.setPushOptions(pushOptions);
PushOneCommit.Result r = push.to("refs/for/master");
r.assertOkStatus();
r.assertChange(Change.Status.NEW, "myTopic");
r.assertPushOptions(pushOptions);
}
@Test @Test
public void pushForMasterWithNotify() throws Exception { public void pushForMasterWithNotify() throws Exception {
TestAccount user2 = accounts.user2(); TestAccount user2 = accounts.user2();

View File

@@ -42,6 +42,7 @@ import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashBiMap; import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultimap; import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
@@ -315,6 +316,8 @@ public class ReceiveCommits {
private final RequestId receiveId; private final RequestId receiveId;
private MagicBranchInput magicBranch; private MagicBranchInput magicBranch;
private boolean newChangeForAllNotInTarget; private boolean newChangeForAllNotInTarget;
private final ListMultimap<String, String> pushOptions =
LinkedListMultimap.create();
private List<CreateRequest> newChanges = Collections.emptyList(); private List<CreateRequest> newChanges = Collections.emptyList();
private final Map<Change.Id, ReplaceRequest> replaceByChange = private final Map<Change.Id, ReplaceRequest> replaceByChange =
@@ -490,6 +493,7 @@ public class ReceiveCommits {
advHooks.add(new HackPushNegotiateHook()); advHooks.add(new HackPushNegotiateHook());
rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks)); rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
rp.setPostReceiveHook(lazyPostReceive.get()); rp.setPostReceiveHook(lazyPostReceive.get());
rp.setAllowPushOptions(true);
} }
public void init() { public void init() {
@@ -915,6 +919,18 @@ public class ReceiveCommits {
} }
private void parseCommands(Collection<ReceiveCommand> commands) { private void parseCommands(Collection<ReceiveCommand> commands) {
List<String> optionList = rp.getPushOptions();
if (optionList != null) {
for (String option : optionList) {
int e = option.indexOf('=');
if (e > 0) {
pushOptions.put(option.substring(0, e), option.substring(e + 1));
} else {
pushOptions.put(option, "");
}
}
}
logDebug("Parsing {} commands", commands.size()); logDebug("Parsing {} commands", commands.size());
for (ReceiveCommand cmd : commands) { for (ReceiveCommand cmd : commands) {
if (cmd.getResult() != NOT_ATTEMPTED) { if (cmd.getResult() != NOT_ATTEMPTED) {
@@ -1305,14 +1321,14 @@ public class ReceiveCommits {
return new MailRecipients(reviewer, cc); return new MailRecipients(reviewer, cc);
} }
String parse(CmdLineParser clp, Repository repo, Set<String> refs) String parse(CmdLineParser clp, Repository repo, Set<String> refs,
throws CmdLineException { ListMultimap<String, String> pushOptions) throws CmdLineException {
String ref = RefNames.fullName( String ref = RefNames.fullName(
MagicBranch.getDestBranchName(cmd.getRefName())); MagicBranch.getDestBranchName(cmd.getRefName()));
ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions);
int optionStart = ref.indexOf('%'); int optionStart = ref.indexOf('%');
if (0 < optionStart) { if (0 < optionStart) {
ListMultimap<String, String> options = LinkedListMultimap.create();
for (String s : COMMAS.split(ref.substring(optionStart + 1))) { for (String s : COMMAS.split(ref.substring(optionStart + 1))) {
int e = s.indexOf('='); int e = s.indexOf('=');
if (0 < e) { if (0 < e) {
@@ -1321,10 +1337,13 @@ public class ReceiveCommits {
options.put(s, ""); options.put(s, "");
} }
} }
clp.parseOptionMap(options);
ref = ref.substring(0, optionStart); ref = ref.substring(0, optionStart);
} }
if (!options.isEmpty()) {
clp.parseOptionMap(options);
}
// Split the destination branch by branch and topic. The topic // Split the destination branch by branch and topic. The topic
// suffix is entirely optional, so it might not even exist. // suffix is entirely optional, so it might not even exist.
String head = readHEAD(repo); String head = readHEAD(repo);
@@ -1347,6 +1366,19 @@ public class ReceiveCommits {
} }
} }
/**
* Gets an unmodifiable view of the pushOptions.
* <p>
* The collection is empty if the client does not support push options, or if
* the client did not send any options.
*
* @return an unmodifiable view of pushOptions.
*/
@Nullable
public ListMultimap<String, String> getPushOptions() {
return ImmutableListMultimap.copyOf(pushOptions);
}
private void parseMagicBranch(ReceiveCommand cmd) { private void parseMagicBranch(ReceiveCommand cmd) {
// Permit exactly one new change request per push. // Permit exactly one new change request per push.
if (magicBranch != null) { if (magicBranch != null) {
@@ -1362,8 +1394,10 @@ public class ReceiveCommits {
String ref; String ref;
CmdLineParser clp = optionParserFactory.create(magicBranch); CmdLineParser clp = optionParserFactory.create(magicBranch);
magicBranch.clp = clp; magicBranch.clp = clp;
try { try {
ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet()); ref = magicBranch.parse(
clp, repo, rp.getAdvertisedRefs().keySet(), pushOptions);
} catch (CmdLineException e) { } catch (CmdLineException e) {
if (!clp.wasHelpRequestedByOption()) { if (!clp.wasHelpRequestedByOption()) {
logDebug("Invalid branch syntax"); logDebug("Invalid branch syntax");