blob: 963fe29fdec14febbc7a28f54e8f32e8a811a253 [file] [log] [blame]
// 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.acceptance.git;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.cloneProject;
import static com.google.gerrit.acceptance.GitUtil.createCommit;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.GitUtil.Commit;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.jcraft.jsch.JSchException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.transport.PushResult;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeUtils.MillisProvider;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
public abstract class AbstractPushForReview extends AbstractDaemonTest {
@ConfigSuite.Config
public static Config noteDbEnabled() {
return NotesMigration.allEnabledConfig();
}
@Inject
private NotesMigration notesMigration;
protected enum Protocol {
SSH, HTTP
}
private String sshUrl;
@BeforeClass
public static void setTimeForTesting() {
final long clockStepMs = MILLISECONDS.convert(1, SECONDS);
final AtomicLong clockMs = new AtomicLong(
new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
@Override
public long getMillis() {
return clockMs.getAndAdd(clockStepMs);
}
});
}
@AfterClass
public static void restoreTime() {
DateTimeUtils.setCurrentMillisSystem();
}
@Before
public void setUp() throws Exception {
sshUrl = sshSession.getUrl();
}
protected void selectProtocol(Protocol p) throws GitAPIException, IOException {
String url;
switch (p) {
case SSH:
url = sshUrl;
break;
case HTTP:
url = admin.getHttpUrl(server);
break;
default:
throw new IllegalArgumentException("unexpected protocol: " + p);
}
git = cloneProject(url + "/" + project.get());
}
@Test
public void testPushForMaster() throws GitAPIException, OrmException,
IOException {
PushOneCommit.Result r = pushTo("refs/for/master");
r.assertOkStatus();
r.assertChange(Change.Status.NEW, null);
}
@Test
public void testPushForMasterWithTopic() throws GitAPIException,
OrmException, IOException {
// specify topic in ref
String topic = "my/topic";
PushOneCommit.Result r = pushTo("refs/for/master/" + topic);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, topic);
// specify topic as option
r = pushTo("refs/for/master%topic=" + topic);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, topic);
}
@Test
public void testPushForMasterWithCc() throws GitAPIException, OrmException,
IOException, JSchException {
// cc one user
String topic = "my/topic";
PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, topic);
// cc several users
TestAccount user2 =
accounts.create("another-user", "another.user@example.com", "Another User");
r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+ user.email + ",cc=" + user2.email);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, topic);
// cc non-existing user
String nonExistingEmail = "non.existing@example.com";
r = pushTo("refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+ nonExistingEmail + ",cc=" + user.email);
r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
}
@Test
public void testPushForMasterWithReviewer() throws GitAPIException,
OrmException, IOException, JSchException {
// add one reviewer
String topic = "my/topic";
PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, topic, user);
// add several reviewers
TestAccount user2 =
accounts.create("another-user", "another.user@example.com", "Another User");
r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
+ ",r=" + user2.email);
r.assertOkStatus();
// admin is the owner of the change and should not appear as reviewer
r.assertChange(Change.Status.NEW, topic, user, user2);
// add non-existing user as reviewer
String nonExistingEmail = "non.existing@example.com";
r = pushTo("refs/for/master/" + topic + "%r=" + admin.email + ",r="
+ nonExistingEmail + ",r=" + user.email);
r.assertErrorStatus("user \"" + nonExistingEmail + "\" not found");
}
@Test
public void testPushForMasterAsDraft() throws GitAPIException, OrmException,
IOException {
// create draft by pushing to 'refs/drafts/'
PushOneCommit.Result r = pushTo("refs/drafts/master");
r.assertOkStatus();
r.assertChange(Change.Status.DRAFT, null);
// create draft by using 'draft' option
r = pushTo("refs/for/master%draft");
r.assertOkStatus();
r.assertChange(Change.Status.DRAFT, null);
}
@Test
public void testPushForMasterAsEdit() throws GitAPIException,
IOException, RestApiException {
PushOneCommit.Result r = pushTo("refs/for/master");
r.assertOkStatus();
EditInfo edit = getEdit(r.getChangeId());
assertThat(edit).isNull();
// specify edit as option
r = amendChange(r.getChangeId(), "refs/for/master%edit");
r.assertOkStatus();
edit = getEdit(r.getChangeId());
assertThat(edit).isNotNull();
}
@Test
public void testPushForMasterWithApprovals() throws GitAPIException,
IOException, RestApiException {
PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review");
r.assertOkStatus();
ChangeInfo ci = get(r.getChangeId());
LabelInfo cr = ci.labels.get("Code-Review");
assertThat(cr.all).hasSize(1);
assertThat(cr.all.get(0).name).isEqualTo("Administrator");
assertThat(cr.all.get(0).value.intValue()).is(1);
assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
"Uploaded patch set 1: Code-Review+1.");
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
"b.txt", "anotherContent", r.getChangeId());
r = push.to(git, "refs/for/master/%l=Code-Review+2");
ci = get(r.getChangeId());
cr = ci.labels.get("Code-Review");
assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
"Uploaded patch set 2: Code-Review+2.");
assertThat(cr.all).hasSize(1);
assertThat(cr.all.get(0).name).isEqualTo("Administrator");
assertThat(cr.all.get(0).value.intValue()).is(2);
push =
pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
"c.txt", "moreContent", r.getChangeId());
r = push.to(git, "refs/for/master/%l=Code-Review+2");
ci = get(r.getChangeId());
assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
"Uploaded patch set 3.");
}
/**
* There was a bug that allowed a user with Forge Committer Identity access
* right to upload a commit and put *votes on behalf of another user* on it.
* This test checks that this is not possible, but that the votes that are
* specified on push are applied only in the name of the uploader.
*
* This particular bug only occurred when there was more than one label
* defined. Hence the test defines a custom label.
*
* When on upload the committer is forged he is automatically added as
* reviewer to the change. This results in a dummy 0 vote. If there was only
* one label this dummy 0 vote collided with any vote for the same label that
* was specified in the push specification and hence the upload failed, which
* means in this case it was not possible to forge a vote.
*/
@Test
public void testPushForMasterWithApprovalsForgeCommitterButNoForgeVote()
throws GitAPIException, IOException, RestApiException {
// add custom label because the bug only allowed to forge a vote when there
// were at least two labels
LabelType Q = category("CustomLabel",
value(1, "Positive"),
value(0, "No score"),
value(-1, "Negative"));
ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
cfg.getLabelSections().put(Q.getName(), Q);
MetaDataUpdate md = metaDataUpdateFactory.create(allProjects);
try {
cfg.commit(md);
} finally {
md.close();
}
projectCache.evict(allProjects);
// Create a commit with "User" as author and committer
Commit c = createCommit(git, user.getIdent(), PushOneCommit.SUBJECT);
// Push this commit as "Administrator" (requires Forge Committer Identity)
pushHead(git, "refs/for/master/%l=Code-Review+1", false);
// Expected Code-Review votes:
// 1. 0 from User (committer):
// When the committer is forged, the committer is automatically added as
// reviewer, hence we expect a dummy 0 vote for the committer.
// 2. +1 from Administrator (uploader):
// On push Code-Review+1 was specified, hence we expect a +1 vote from
// the uploader.
ChangeInfo ci = get(c.getChangeId());
LabelInfo cr = ci.labels.get("Code-Review");
assertThat(cr.all).hasSize(2);
assertThat(cr.all.get(0).name).isEqualTo("Administrator");
assertThat(cr.all.get(0).value.intValue()).is(1);
assertThat(cr.all.get(1).name).isEqualTo("User");
assertThat(cr.all.get(1).value.intValue()).is(0);
assertThat(Iterables.getLast(ci.messages).message).isEqualTo(
"Uploaded patch set 1: Code-Review+1.");
}
@Test
public void testPushNewPatchsetToRefsChanges() throws GitAPIException,
IOException, OrmException {
PushOneCommit.Result r = pushTo("refs/for/master");
r.assertOkStatus();
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
"b.txt", "anotherContent", r.getChangeId());
r = push.to(git, "refs/changes/" + r.getChange().change().getId().get());
r.assertOkStatus();
}
@Test
public void testPushForMasterWithApprovals_MissingLabel() throws GitAPIException,
IOException {
PushOneCommit.Result r = pushTo("refs/for/master/%l=Verify");
r.assertErrorStatus("label \"Verify\" is not a configured label");
}
@Test
public void testPushForMasterWithApprovals_ValueOutOfRange() throws GitAPIException,
IOException {
PushOneCommit.Result r = pushTo("refs/for/master/%l=Code-Review-3");
r.assertErrorStatus("label \"Code-Review\": -3 is not a valid value");
}
@Test
public void testPushForNonExistingBranch() throws GitAPIException,
IOException {
String branchName = "non-existing";
PushOneCommit.Result r = pushTo("refs/for/" + branchName);
r.assertErrorStatus("branch " + branchName + " not found");
}
@Test
public void testPushForMasterWithHashtags() throws GitAPIException,
OrmException, IOException, RestApiException {
// Hashtags currently only work when noteDB is enabled
assume().that(notesMigration.enabled()).isTrue();
// specify a single hashtag as option
String hashtag1 = "tag1";
Set<String> expected = ImmutableSet.of(hashtag1);
PushOneCommit.Result r = pushTo("refs/for/master%hashtag=#" + hashtag1);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, null);
Set<String> hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
// specify a single hashtag as option in new patch set
String hashtag2 = "tag2";
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
"b.txt", "anotherContent", r.getChangeId());
r = push.to(git, "refs/for/master/%hashtag=" + hashtag2);
r.assertOkStatus();
expected = ImmutableSet.of(hashtag1, hashtag2);
hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
}
@Test
public void testPushForMasterWithMultipleHashtags() throws GitAPIException,
OrmException, IOException, RestApiException {
// Hashtags currently only work when noteDB is enabled
assume().that(notesMigration.enabled()).isTrue();
// specify multiple hashtags as options
String hashtag1 = "tag1";
String hashtag2 = "tag2";
Set<String> expected = ImmutableSet.of(hashtag1, hashtag2);
PushOneCommit.Result r = pushTo("refs/for/master%hashtag=#" + hashtag1
+ ",hashtag=##" + hashtag2);
r.assertOkStatus();
r.assertChange(Change.Status.NEW, null);
Set<String> hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
// specify multiple hashtags as options in new patch set
String hashtag3 = "tag3";
String hashtag4 = "tag4";
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
"b.txt", "anotherContent", r.getChangeId());
r = push.to(git,
"refs/for/master%hashtag=" + hashtag3 + ",hashtag=" + hashtag4);
r.assertOkStatus();
expected = ImmutableSet.of(hashtag1, hashtag2, hashtag3, hashtag4);
hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
}
@Test
public void testPushForMasterWithHashtagsNoteDbDisabled() throws GitAPIException,
IOException {
// push with hashtags should fail when noteDb is disabled
assume().that(notesMigration.enabled()).isFalse();
PushOneCommit.Result r = pushTo("refs/for/master%hashtag=tag1");
r.assertErrorStatus("cannot add hashtags; noteDb is disabled");
}
@Test
public void testPushSameCommitTwiceUsingMagicBranchBaseOption()
throws Exception {
grant(Permission.PUSH, project, "refs/heads/master");
PushOneCommit.Result rBase = pushTo("refs/heads/master");
rBase.assertOkStatus();
gApi.projects()
.name(project.get())
.branch("foo")
.create(new BranchInput());
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
"b.txt", "anotherContent");
PushOneCommit.Result r = push.to(git, "refs/for/master");
r.assertOkStatus();
PushResult pr = GitUtil.pushHead(
git, "refs/for/foo%base=" + rBase.getCommitId().name(), false, false);
assertThat(pr.getMessages()).contains("changes: new: 1, refs: 1, done");
List<ChangeInfo> changes = query(r.getCommitId().name());
assertThat(changes).hasSize(2);
ChangeInfo c1 = get(changes.get(0).id);
ChangeInfo c2 = get(changes.get(1).id);
assertThat(c1.project).isEqualTo(c2.project);
assertThat(c1.branch).isNotEqualTo(c2.branch);
assertThat(c1.changeId).isEqualTo(c2.changeId);
assertThat(c1.currentRevision).isEqualTo(c2.currentRevision);
}
@Test
public void testPushCommitUsingSignedOffBy() throws Exception {
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
"b.txt", "anotherContent");
PushOneCommit.Result r = push.to(git, "refs/for/master");
r.assertOkStatus();
setUseSignedOffBy(InheritableBoolean.TRUE);
blockForgeCommitter(project, "refs/heads/master");
push = pushFactory.create(db, admin.getIdent(),
PushOneCommit.SUBJECT + String.format(
"\n\nSigned-off-by: %s <%s>", admin.fullName, admin.email),
"b.txt", "anotherContent");
r = push.to(git, "refs/for/master");
r.assertOkStatus();
push = pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
"b.txt", "anotherContent");
r = push.to(git, "refs/for/master");
r.assertErrorStatus(
"not Signed-off-by author/committer/uploader in commit message footer");
}
}