Merge "Add a RefFilterOption to always return all most recent ref changes"
This commit is contained in:
@@ -27,7 +27,6 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.entities.BranchNameKey;
|
||||
import com.google.gerrit.entities.Change;
|
||||
import com.google.gerrit.entities.Project;
|
||||
import com.google.gerrit.entities.RefNames;
|
||||
@@ -57,9 +56,12 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectIdRef;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Ref.Storage;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
|
||||
class DefaultRefFilter {
|
||||
@@ -82,7 +84,7 @@ class DefaultRefFilter {
|
||||
private final Counter0 skipFilterCount;
|
||||
private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
|
||||
|
||||
private Map<Change.Id, BranchNameKey> visibleChanges;
|
||||
private Map<Change.Id, ChangeNotes> visibleChanges;
|
||||
|
||||
@Inject
|
||||
DefaultRefFilter(
|
||||
@@ -135,6 +137,15 @@ class DefaultRefFilter {
|
||||
"Project state %s permits read = %s",
|
||||
projectState.getProject().getState(), projectState.statePermitsRead());
|
||||
|
||||
// If we anyway always return all available (most recent) changes in the change index and cache,
|
||||
// we shouldn't care about refs/changes.
|
||||
if (opts.returnMostRecentRefChanges()) {
|
||||
refs =
|
||||
refs.stream()
|
||||
.filter(r -> !RefNames.isRefsChanges(r.getName()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// See if we can get away with a single, cheap ref evaluation.
|
||||
if (refs.size() == 1) {
|
||||
String refName = Iterables.getOnlyElement(refs).getName();
|
||||
@@ -142,7 +153,7 @@ class DefaultRefFilter {
|
||||
logger.atFinest().log("Filter out metadata ref %s", refName);
|
||||
return ImmutableList.of();
|
||||
}
|
||||
if (RefNames.isRefsChanges(refName)) {
|
||||
if (RefNames.isRefsChanges(refName) && !opts.returnMostRecentRefChanges()) {
|
||||
boolean isChangeRefVisisble = canSeeSingleChangeRef(refName);
|
||||
if (isChangeRefVisisble) {
|
||||
logger.atFinest().log("Change ref %s is visible", refName);
|
||||
@@ -185,10 +196,31 @@ class DefaultRefFilter {
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.returnMostRecentRefChanges()) {
|
||||
visibleChanges(repo).values().stream()
|
||||
.forEach(n -> addAllChangeAndPatchsetRefs(visibleRefs, n));
|
||||
}
|
||||
|
||||
logger.atFinest().log("visible refs = %s", visibleRefs);
|
||||
return visibleRefs;
|
||||
}
|
||||
|
||||
private void addAllChangeAndPatchsetRefs(Collection<Ref> refs, ChangeNotes changeNotes) {
|
||||
refs.add(
|
||||
new ObjectIdRef.PeeledNonTag(
|
||||
Storage.PACKED,
|
||||
RefNames.changeMetaRef(changeNotes.getChangeId()),
|
||||
changeNotes.getMetaId()));
|
||||
changeNotes
|
||||
.getPatchSets()
|
||||
.values()
|
||||
.forEach(
|
||||
p ->
|
||||
refs.add(
|
||||
new ObjectIdRef.PeeledNonTag(
|
||||
Storage.PACKED, RefNames.patchSetRef(p.id()), p.commitId())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters refs by visibility. Returns tags where visibility can't be trivially computed
|
||||
* separately for later rev-walk-based visibility computation. Tags where visibility is trivial to
|
||||
@@ -256,7 +288,8 @@ class DefaultRefFilter {
|
||||
} else if ((changeId = Change.Id.fromRef(refName)) != null) {
|
||||
// This is a mere performance optimization. RefVisibilityControl could determine the
|
||||
// visibility of these refs just fine. But instead, we use highly-optimized logic that
|
||||
// looks only on the last 10k most recent changes using the change index and a cache.
|
||||
// looks only on the available changes in the change index and cache (which are the
|
||||
// most recent changes).
|
||||
if (hasAccessDatabase) {
|
||||
resultRefs.add(ref);
|
||||
} else if (!visible(repo, changeId)) {
|
||||
@@ -315,6 +348,11 @@ class DefaultRefFilter {
|
||||
}
|
||||
|
||||
private boolean visible(Repository repo, Change.Id changeId) throws PermissionBackendException {
|
||||
return visibleChanges(repo).containsKey(changeId);
|
||||
}
|
||||
|
||||
private Map<Change.Id, ChangeNotes> visibleChanges(Repository repo)
|
||||
throws PermissionBackendException {
|
||||
if (visibleChanges == null) {
|
||||
if (changeCache == null) {
|
||||
visibleChanges = visibleChangesByScan(repo);
|
||||
@@ -323,7 +361,7 @@ class DefaultRefFilter {
|
||||
}
|
||||
logger.atFinest().log("Visible changes: %s", visibleChanges.keySet());
|
||||
}
|
||||
return visibleChanges.containsKey(changeId);
|
||||
return visibleChanges;
|
||||
}
|
||||
|
||||
private boolean visibleEdit(Repository repo, String name) throws PermissionBackendException {
|
||||
@@ -340,15 +378,11 @@ class DefaultRefFilter {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Initialize visibleChanges if it wasn't initialized yet.
|
||||
if (visibleChanges == null) {
|
||||
visible(repo, id);
|
||||
}
|
||||
if (visibleChanges.containsKey(id)) {
|
||||
if (visible(repo, id)) {
|
||||
try {
|
||||
// Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
|
||||
permissionBackendForProject
|
||||
.ref(visibleChanges.get(id).branch())
|
||||
.ref(visibleChanges(repo).get(id).getChange().getDest().branch())
|
||||
.check(RefPermission.READ_PRIVATE_CHANGES);
|
||||
logger.atFinest().log("Foreign change edit ref is visible: %s", name);
|
||||
return true;
|
||||
@@ -362,17 +396,17 @@ class DefaultRefFilter {
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<Change.Id, BranchNameKey> visibleChangesBySearch() throws PermissionBackendException {
|
||||
private Map<Change.Id, ChangeNotes> visibleChangesBySearch() throws PermissionBackendException {
|
||||
Project.NameKey project = projectState.getNameKey();
|
||||
try {
|
||||
Map<Change.Id, BranchNameKey> visibleChanges = new HashMap<>();
|
||||
Map<Change.Id, ChangeNotes> visibleChanges = new HashMap<>();
|
||||
for (ChangeData cd : changeCache.getChangeData(project)) {
|
||||
if (!projectState.statePermitsRead()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
permissionBackendForProject.change(cd).check(ChangePermission.READ);
|
||||
visibleChanges.put(cd.getId(), cd.change().getDest());
|
||||
visibleChanges.put(cd.getId(), cd.notes());
|
||||
} catch (AuthException e) {
|
||||
// Do nothing.
|
||||
}
|
||||
@@ -385,7 +419,7 @@ class DefaultRefFilter {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Change.Id, BranchNameKey> visibleChangesByScan(Repository repo)
|
||||
private Map<Change.Id, ChangeNotes> visibleChangesByScan(Repository repo)
|
||||
throws PermissionBackendException {
|
||||
Project.NameKey p = projectState.getNameKey();
|
||||
ImmutableList<ChangeNotesResult> changes;
|
||||
@@ -397,11 +431,11 @@ class DefaultRefFilter {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<Change.Id, BranchNameKey> result = Maps.newHashMapWithExpectedSize(changes.size());
|
||||
Map<Change.Id, ChangeNotes> result = Maps.newHashMapWithExpectedSize(changes.size());
|
||||
for (ChangeNotesResult notesResult : changes) {
|
||||
ChangeNotes notes = toNotes(notesResult);
|
||||
if (notes != null) {
|
||||
result.put(notes.getChangeId(), notes.getChange().getDest());
|
||||
result.put(notes.getChangeId(), notes);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -329,6 +329,13 @@ public abstract class PermissionBackend {
|
||||
/** Remove all NoteDb refs (refs/changes/*, refs/users/*, edit refs) from the result. */
|
||||
public abstract boolean filterMeta();
|
||||
|
||||
/**
|
||||
* Return all of the visible change refs that are available in the change index (which are the
|
||||
* most recent changes), even if they are not part of the List<Ref> passed. This allows the
|
||||
* caller not to send all the refs/changes.
|
||||
*/
|
||||
public abstract boolean returnMostRecentRefChanges();
|
||||
|
||||
/**
|
||||
* Select only refs with names matching prefixes per {@link
|
||||
* org.eclipse.jgit.lib.RefDatabase#getRefsByPrefix}.
|
||||
@@ -340,6 +347,7 @@ public abstract class PermissionBackend {
|
||||
public static Builder builder() {
|
||||
return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
|
||||
.setFilterMeta(false)
|
||||
.setReturnMostRecentRefChanges(false)
|
||||
.setPrefixes(Collections.singletonList(""));
|
||||
}
|
||||
|
||||
@@ -347,6 +355,8 @@ public abstract class PermissionBackend {
|
||||
public abstract static class Builder {
|
||||
public abstract Builder setFilterMeta(boolean val);
|
||||
|
||||
public abstract Builder setReturnMostRecentRefChanges(boolean val);
|
||||
|
||||
public abstract Builder setPrefixes(List<String> prefixes);
|
||||
|
||||
public abstract RefFilterOptions build();
|
||||
|
||||
@@ -1422,6 +1422,180 @@ public class RefAdvertisementIT extends AbstractDaemonTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertiseMostRecentRefChangesEvenWhenNotInInputWithRefStarPermission()
|
||||
throws Exception {
|
||||
// admin has refs/* permission.
|
||||
requestScopeOperations.setApiUser(admin.id());
|
||||
|
||||
try (Repository repo = repoManager.openRepository(project)) {
|
||||
PermissionBackend.ForProject forProject = newFilter(project, admin);
|
||||
assertThat(
|
||||
names(
|
||||
forProject.filter(
|
||||
// set empty list of refs to filter
|
||||
new ArrayList<>(),
|
||||
repo,
|
||||
RefFilterOptions.builder().setReturnMostRecentRefChanges(true).build())))
|
||||
// all the change refs are still returned since returnMostRecentRefChanges = true
|
||||
.containsExactlyElementsIn(
|
||||
ImmutableList.of(
|
||||
psRef1, metaRef1, psRef2, metaRef2, psRef3, metaRef3, psRef4, metaRef4));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertiseMostRecentRefChangesEvenWhenNotInInputWithoutRefStarPermission()
|
||||
throws Exception {
|
||||
projectOperations
|
||||
.project(project)
|
||||
.forUpdate()
|
||||
.add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
|
||||
.update();
|
||||
|
||||
// user doesn't have refs/* permission.
|
||||
requestScopeOperations.setApiUser(user.id());
|
||||
|
||||
try (Repository repo = repoManager.openRepository(project)) {
|
||||
PermissionBackend.ForProject forProject = newFilter(project, admin);
|
||||
assertThat(
|
||||
names(
|
||||
forProject.filter(
|
||||
// set empty list of refs to filter
|
||||
new ArrayList<>(),
|
||||
repo,
|
||||
RefFilterOptions.builder().setReturnMostRecentRefChanges(true).build())))
|
||||
// all the change refs are still returned since returnMostRecentRefChanges = true
|
||||
.containsExactlyElementsIn(
|
||||
ImmutableList.of(
|
||||
psRef1, metaRef1, psRef2, metaRef2, psRef3, metaRef3, psRef4, metaRef4));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertiseMostRecentRefChangesOnlyOnceWithRefStarPermission() throws Exception {
|
||||
// admin has refs/* permission.
|
||||
requestScopeOperations.setApiUser(admin.id());
|
||||
|
||||
try (Repository repo = repoManager.openRepository(project)) {
|
||||
PermissionBackend.ForProject forProject = newFilter(project, admin);
|
||||
assertThat(
|
||||
names(
|
||||
forProject.filter(
|
||||
repo.getRefDatabase().getRefs(),
|
||||
repo,
|
||||
RefFilterOptions.builder().setReturnMostRecentRefChanges(true).build())))
|
||||
// all the change refs are still returned since returnMostRecentRefChanges = true. Make
|
||||
// sure they are only returned once.
|
||||
.containsExactlyElementsIn(
|
||||
ImmutableList.of(
|
||||
"HEAD",
|
||||
psRef1,
|
||||
metaRef1,
|
||||
psRef2,
|
||||
metaRef2,
|
||||
psRef3,
|
||||
metaRef3,
|
||||
psRef4,
|
||||
metaRef4,
|
||||
"refs/heads/branch",
|
||||
"refs/heads/master",
|
||||
"refs/meta/config",
|
||||
"refs/tags/branch-tag",
|
||||
"refs/tags/master-tag",
|
||||
"refs/tags/tree-tag"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertiseMostRecentRefChangesOnlyOnceWithoutRefStarPermission() throws Exception {
|
||||
projectOperations
|
||||
.project(project)
|
||||
.forUpdate()
|
||||
.add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
|
||||
.update();
|
||||
|
||||
// user doesn't have refs/* permission.
|
||||
requestScopeOperations.setApiUser(user.id());
|
||||
|
||||
try (Repository repo = repoManager.openRepository(project)) {
|
||||
PermissionBackend.ForProject forProject = newFilter(project, admin);
|
||||
assertThat(
|
||||
names(
|
||||
forProject.filter(
|
||||
repo.getRefDatabase().getRefs(),
|
||||
repo,
|
||||
RefFilterOptions.builder().setReturnMostRecentRefChanges(true).build())))
|
||||
// all the change refs are still returned since returnMostRecentRefChanges = true. Make
|
||||
// sure they are only returned once.
|
||||
.containsExactlyElementsIn(
|
||||
ImmutableList.of(
|
||||
"HEAD",
|
||||
psRef1,
|
||||
metaRef1,
|
||||
psRef2,
|
||||
metaRef2,
|
||||
psRef3,
|
||||
metaRef3,
|
||||
psRef4,
|
||||
metaRef4,
|
||||
"refs/heads/branch",
|
||||
"refs/heads/master",
|
||||
"refs/meta/config",
|
||||
"refs/tags/branch-tag",
|
||||
"refs/tags/master-tag",
|
||||
"refs/tags/tree-tag"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertiseMostRecentRefChangesWithSingleRequestedRefWithRefStarPermission()
|
||||
throws Exception {
|
||||
// admin has refs/* permission.
|
||||
requestScopeOperations.setApiUser(admin.id());
|
||||
|
||||
try (Repository repo = repoManager.openRepository(project)) {
|
||||
PermissionBackend.ForProject forProject = newFilter(project, admin);
|
||||
assertThat(
|
||||
names(
|
||||
forProject.filter(
|
||||
ImmutableList.of(repo.exactRef("HEAD")),
|
||||
repo,
|
||||
RefFilterOptions.builder().setReturnMostRecentRefChanges(true).build())))
|
||||
// all the change refs are still returned since returnMostRecentRefChanges = true.
|
||||
.containsExactlyElementsIn(
|
||||
ImmutableList.of(
|
||||
"HEAD", psRef1, metaRef1, psRef2, metaRef2, psRef3, metaRef3, psRef4, metaRef4));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void advertiseMostRecentRefChangesWithSingleRefRequetedWithoutRefStarPermission()
|
||||
throws Exception {
|
||||
projectOperations
|
||||
.project(project)
|
||||
.forUpdate()
|
||||
.add(allow(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
|
||||
.update();
|
||||
|
||||
// user doesn't have refs/* permission.
|
||||
requestScopeOperations.setApiUser(user.id());
|
||||
|
||||
try (Repository repo = repoManager.openRepository(project)) {
|
||||
PermissionBackend.ForProject forProject = newFilter(project, admin);
|
||||
assertThat(
|
||||
names(
|
||||
forProject.filter(
|
||||
ImmutableList.of(repo.exactRef("HEAD")),
|
||||
repo,
|
||||
RefFilterOptions.builder().setReturnMostRecentRefChanges(true).build())))
|
||||
// all the change refs are still returned since returnMostRecentRefChanges = true.
|
||||
.containsExactlyElementsIn(
|
||||
ImmutableList.of(
|
||||
"HEAD", psRef1, metaRef1, psRef2, metaRef2, psRef3, metaRef3, psRef4, metaRef4));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fetchSingleChangeWithoutIndexAccess() throws Exception {
|
||||
PushOneCommit.Result change = createChange();
|
||||
|
||||
Reference in New Issue
Block a user