blob: b900cc71a1871a7539d6e8d573402fc3bf76fd92 [file] [log] [blame]
// Copyright (C) 2014 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.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.ReceiveCommitsAdvertiseRefsHook;
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.Util;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testutil.DisabledReviewDb;
import com.google.gerrit.testutil.TestChanges;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.junit.Before;
import org.junit.Test;
@NoHttpd
public class RefAdvertisementIT extends AbstractDaemonTest {
@Inject private ProjectControl.GenericFactory projectControlFactory;
@Inject @Nullable private SearchingChangeCacheImpl changeCache;
@Inject private TagCache tagCache;
@Inject private Provider<CurrentUser> userProvider;
@Inject private ChangeNoteUtil noteUtil;
@Inject @AnonymousCowardName private String anonymousCowardName;
private AccountGroup.UUID admins;
private ChangeData c1;
private ChangeData c2;
private ChangeData c3;
private ChangeData c4;
private String r1;
private String r2;
private String r3;
private String r4;
@Before
public void setUp() throws Exception {
admins = groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID();
setUpPermissions();
setUpChanges();
}
private void setUpPermissions() throws Exception {
// Remove read permissions for all users besides admin. This method is
// idempotent, so is safe to call on every test setup.
ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
for (AccessSection sec : pc.getAccessSections()) {
sec.removePermission(Permission.READ);
}
Util.allow(pc, Permission.READ, admins, "refs/*");
saveProjectConfig(allProjects, pc);
}
private static String changeRefPrefix(Change.Id id) {
String ps = new PatchSet.Id(id, 1).toRefName();
return ps.substring(0, ps.length() - 1);
}
private void setUpChanges() throws Exception {
gApi.projects().name(project.get()).branch("branch").create(new BranchInput());
// First 2 changes are merged, which means the tags pointing to them are
// visible.
allow(Permission.SUBMIT, admins, "refs/for/refs/heads/*");
PushOneCommit.Result mr =
pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master%submit");
mr.assertOkStatus();
c1 = mr.getChange();
r1 = changeRefPrefix(c1.getId());
PushOneCommit.Result br =
pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/branch%submit");
br.assertOkStatus();
c2 = br.getChange();
r2 = changeRefPrefix(c2.getId());
// Second 2 changes are unmerged.
mr = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/master");
mr.assertOkStatus();
c3 = mr.getChange();
r3 = changeRefPrefix(c3.getId());
br = pushFactory.create(db, admin.getIdent(), testRepo).to("refs/for/branch");
br.assertOkStatus();
c4 = br.getChange();
r4 = changeRefPrefix(c4.getId());
try (Repository repo = repoManager.openRepository(project)) {
// master-tag -> master
RefUpdate mtu = repo.updateRef("refs/tags/master-tag");
mtu.setExpectedOldObjectId(ObjectId.zeroId());
mtu.setNewObjectId(repo.exactRef("refs/heads/master").getObjectId());
assertThat(mtu.update()).isEqualTo(RefUpdate.Result.NEW);
// branch-tag -> branch
RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
btu.setExpectedOldObjectId(ObjectId.zeroId());
btu.setNewObjectId(repo.exactRef("refs/heads/branch").getObjectId());
assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
}
}
@Test
public void uploadPackAllRefsVisibleNoRefsMetaConfig() throws Exception {
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
Util.allow(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
Util.allow(cfg, Permission.READ, admins, RefNames.REFS_CONFIG);
Util.doNotInherit(cfg, Permission.READ, RefNames.REFS_CONFIG);
saveProjectConfig(project, cfg);
setApiUser(user);
assertUploadPackRefs(
"HEAD",
r1 + "1",
r1 + "meta",
r2 + "1",
r2 + "meta",
r3 + "1",
r3 + "meta",
r4 + "1",
r4 + "meta",
"refs/heads/branch",
"refs/heads/master",
"refs/tags/branch-tag",
"refs/tags/master-tag");
}
@Test
public void uploadPackAllRefsVisibleWithRefsMetaConfig() throws Exception {
allow(Permission.READ, REGISTERED_USERS, "refs/*");
allow(Permission.READ, REGISTERED_USERS, RefNames.REFS_CONFIG);
assertUploadPackRefs(
"HEAD",
r1 + "1",
r1 + "meta",
r2 + "1",
r2 + "meta",
r3 + "1",
r3 + "meta",
r4 + "1",
r4 + "meta",
"refs/heads/branch",
"refs/heads/master",
RefNames.REFS_CONFIG,
"refs/tags/branch-tag",
"refs/tags/master-tag");
}
@Test
public void uploadPackSubsetOfBranchesVisibleIncludingHead() throws Exception {
allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
setApiUser(user);
assertUploadPackRefs(
"HEAD",
r1 + "1",
r1 + "meta",
r3 + "1",
r3 + "meta",
"refs/heads/master",
"refs/tags/master-tag");
}
@Test
public void uploadPackSubsetOfBranchesVisibleNotIncludingHead() throws Exception {
deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
setApiUser(user);
assertUploadPackRefs(
r2 + "1",
r2 + "meta",
r4 + "1",
r4 + "meta",
"refs/heads/branch",
"refs/tags/branch-tag",
// master branch is not visible but master-tag is reachable from branch
// (since PushOneCommit always bases changes on each other).
"refs/tags/master-tag");
}
@Test
public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception {
allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
Change c = notesFactory.createChecked(db, project, c1.getId()).getChange();
String changeId = c.getKey().get();
// Admin's edit is not visible.
setApiUser(admin);
gApi.changes().id(changeId).edit().create();
// User's edit is visible.
setApiUser(user);
gApi.changes().id(changeId).edit().create();
assertUploadPackRefs(
"HEAD",
r1 + "1",
r1 + "meta",
r3 + "1",
r3 + "meta",
"refs/heads/master",
"refs/tags/master-tag",
"refs/users/01/1000001/edit-" + c1.getId() + "/1");
}
@Test
public void uploadPackSubsetOfRefsVisibleWithAccessDatabase() throws Exception {
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
try {
deny(Permission.READ, REGISTERED_USERS, "refs/heads/master");
allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
String changeId = c1.change().getKey().get();
setApiUser(admin);
gApi.changes().id(changeId).edit().create();
setApiUser(user);
assertUploadPackRefs(
// Change 1 is visible due to accessDatabase capability, even though
// refs/heads/master is not.
r1 + "1",
r1 + "meta",
r2 + "1",
r2 + "meta",
r3 + "1",
r3 + "meta",
r4 + "1",
r4 + "meta",
"refs/heads/branch",
"refs/tags/branch-tag",
// See comment in subsetOfBranchesVisibleNotIncludingHead.
"refs/tags/master-tag",
// All edits are visible due to accessDatabase capability.
"refs/users/00/1000000/edit-" + c1.getId() + "/1");
} finally {
removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
}
}
@Test
public void uploadPackDraftRefs() throws Exception {
allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
PushOneCommit.Result br =
pushFactory.create(db, admin.getIdent(), testRepo).to("refs/drafts/master");
br.assertOkStatus();
Change.Id c5 = br.getChange().getId();
String r5 = changeRefPrefix(c5);
// Only admin can see admin's draft change (5).
setApiUser(admin);
assertUploadPackRefs(
"HEAD",
r1 + "1",
r1 + "meta",
r2 + "1",
r2 + "meta",
r3 + "1",
r3 + "meta",
r4 + "1",
r4 + "meta",
r5 + "1",
r5 + "meta",
"refs/heads/branch",
"refs/heads/master",
RefNames.REFS_CONFIG,
"refs/tags/branch-tag",
"refs/tags/master-tag");
// user can't.
setApiUser(user);
assertUploadPackRefs(
"HEAD",
r1 + "1",
r1 + "meta",
r2 + "1",
r2 + "meta",
r3 + "1",
r3 + "meta",
r4 + "1",
r4 + "meta",
"refs/heads/branch",
"refs/heads/master",
"refs/tags/branch-tag",
"refs/tags/master-tag");
}
@Test
public void uploadPackNoSearchingChangeCacheImpl() throws Exception {
allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
setApiUser(user);
try (Repository repo = repoManager.openRepository(project)) {
assertRefs(
repo,
new VisibleRefFilter(tagCache, notesFactory, null, repo, projectControl(), db, true),
// Can't use stored values from the index so DB must be enabled.
false,
"HEAD",
r1 + "1",
r1 + "meta",
r2 + "1",
r2 + "meta",
r3 + "1",
r3 + "meta",
r4 + "1",
r4 + "meta",
"refs/heads/branch",
"refs/heads/master",
"refs/tags/branch-tag",
"refs/tags/master-tag");
}
}
@Test
public void uploadPackSequencesWithAccessDatabase() throws Exception {
assume().that(notesMigration.readChangeSequence()).isTrue();
try (Repository repo = repoManager.openRepository(allProjects)) {
setApiUser(user);
assertRefs(repo, newFilter(db, repo, allProjects), true);
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
try {
setApiUser(user);
assertRefs(repo, newFilter(db, repo, allProjects), true, "refs/sequences/changes");
} finally {
removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
}
}
}
@Test
public void receivePackListsOpenChangesAsAdditionalHaves() throws Exception {
ReceiveCommitsAdvertiseRefsHook.Result r = getReceivePackRefs();
assertThat(r.allRefs().keySet())
.containsExactly(
// meta refs are excluded even when NoteDb is enabled.
"HEAD",
"refs/heads/branch",
"refs/heads/master",
"refs/meta/config",
"refs/tags/branch-tag",
"refs/tags/master-tag");
assertThat(r.additionalHaves()).containsExactly(obj(c3, 1), obj(c4, 1));
}
@Test
public void receivePackRespectsVisibilityOfOpenChanges() throws Exception {
allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch");
setApiUser(user);
assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c3, 1));
}
@Test
public void receivePackListsOnlyLatestPatchSet() throws Exception {
testRepo.reset(obj(c3, 1));
PushOneCommit.Result r = amendChange(c3.change().getKey().get());
r.assertOkStatus();
c3 = r.getChange();
assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c3, 2), obj(c4, 1));
}
@Test
public void receivePackOmitsMissingObject() throws Exception {
String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
try (Repository repo = repoManager.openRepository(project)) {
TestRepository<?> tr = new TestRepository<>(repo);
String subject = "Subject for missing commit";
Change c = new Change(c3.change());
PatchSet.Id psId = new PatchSet.Id(c3.getId(), 2);
c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) {
PatchSet ps = TestChanges.newPatchSet(psId, rev, admin.getId());
db.patchSets().insert(Collections.singleton(ps));
db.changes().update(Collections.singleton(c));
}
if (notesMigration.commitChangeWrites()) {
PersonIdent committer = serverIdent.get();
PersonIdent author =
noteUtil.newIdent(
accountCache.get(admin.getId()).getAccount(),
committer.getWhen(),
committer,
anonymousCowardName);
tr.branch(RefNames.changeMetaRef(c3.getId()))
.commit()
.author(author)
.committer(committer)
.message(
"Update patch set "
+ psId.get()
+ "\n"
+ "\n"
+ "Patch-set: "
+ psId.get()
+ "\n"
+ "Commit: "
+ rev
+ "\n"
+ "Subject: "
+ subject
+ "\n")
.create();
}
indexer.index(db, c.getProject(), c.getId());
}
assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c4, 1));
}
/**
* Assert that refs seen by a non-admin user match expected.
*
* @param expectedWithMeta expected refs, in order. If NoteDb is disabled by the configuration,
* any NoteDb refs (i.e. ending in "/meta") are removed from the expected list before
* comparing to the actual results.
* @throws Exception
*/
private void assertUploadPackRefs(String... expectedWithMeta) throws Exception {
try (Repository repo = repoManager.openRepository(project)) {
assertRefs(
repo,
new VisibleRefFilter(
tagCache,
notesFactory,
changeCache,
repo,
projectControl(),
new DisabledReviewDb(),
true),
true,
expectedWithMeta);
}
}
private void assertRefs(
Repository repo, VisibleRefFilter filter, boolean disableDb, String... expectedWithMeta)
throws Exception {
List<String> expected = new ArrayList<>(expectedWithMeta.length);
for (String r : expectedWithMeta) {
if (notesMigration.writeChanges() || !r.endsWith(RefNames.META_SUFFIX)) {
expected.add(r);
}
}
AcceptanceTestRequestScope.Context ctx = null;
if (disableDb) {
ctx = disableDb();
}
try {
Map<String, Ref> all = repo.getAllRefs();
assertThat(filter.filter(all, false).keySet()).containsExactlyElementsIn(expected);
} finally {
if (disableDb) {
enableDb(ctx);
}
}
}
private ReceiveCommitsAdvertiseRefsHook.Result getReceivePackRefs() throws Exception {
ReceiveCommitsAdvertiseRefsHook hook =
new ReceiveCommitsAdvertiseRefsHook(queryProvider, project);
try (Repository repo = repoManager.openRepository(project)) {
return hook.advertiseRefs(repo.getAllRefs());
}
}
private ProjectControl projectControl() throws Exception {
return projectControlFactory.controlFor(project, userProvider.get());
}
private VisibleRefFilter newFilter(ReviewDb db, Repository repo, Project.NameKey project)
throws Exception {
return new VisibleRefFilter(
tagCache,
notesFactory,
null,
repo,
projectControlFactory.controlFor(project, userProvider.get()),
db,
true);
}
private static ObjectId obj(ChangeData cd, int psNum) throws Exception {
PatchSet.Id psId = new PatchSet.Id(cd.getId(), psNum);
PatchSet ps = cd.patchSet(psId);
assertWithMessage("%s not found in %s", psId, cd.patchSets()).that(ps).isNotNull();
return ObjectId.fromString(ps.getRevision().get());
}
}