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.collect.Maps;
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
import com.google.gerrit.common.Nullable;
|
import com.google.gerrit.common.Nullable;
|
||||||
import com.google.gerrit.entities.BranchNameKey;
|
|
||||||
import com.google.gerrit.entities.Change;
|
import com.google.gerrit.entities.Change;
|
||||||
import com.google.gerrit.entities.Project;
|
import com.google.gerrit.entities.Project;
|
||||||
import com.google.gerrit.entities.RefNames;
|
import com.google.gerrit.entities.RefNames;
|
||||||
@@ -57,9 +56,12 @@ import java.util.Collections;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.eclipse.jgit.lib.Config;
|
import org.eclipse.jgit.lib.Config;
|
||||||
import org.eclipse.jgit.lib.Constants;
|
import org.eclipse.jgit.lib.Constants;
|
||||||
|
import org.eclipse.jgit.lib.ObjectIdRef;
|
||||||
import org.eclipse.jgit.lib.Ref;
|
import org.eclipse.jgit.lib.Ref;
|
||||||
|
import org.eclipse.jgit.lib.Ref.Storage;
|
||||||
import org.eclipse.jgit.lib.Repository;
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
|
||||||
class DefaultRefFilter {
|
class DefaultRefFilter {
|
||||||
@@ -82,7 +84,7 @@ class DefaultRefFilter {
|
|||||||
private final Counter0 skipFilterCount;
|
private final Counter0 skipFilterCount;
|
||||||
private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
|
private final boolean skipFullRefEvaluationIfAllRefsAreVisible;
|
||||||
|
|
||||||
private Map<Change.Id, BranchNameKey> visibleChanges;
|
private Map<Change.Id, ChangeNotes> visibleChanges;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
DefaultRefFilter(
|
DefaultRefFilter(
|
||||||
@@ -135,6 +137,15 @@ class DefaultRefFilter {
|
|||||||
"Project state %s permits read = %s",
|
"Project state %s permits read = %s",
|
||||||
projectState.getProject().getState(), projectState.statePermitsRead());
|
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.
|
// See if we can get away with a single, cheap ref evaluation.
|
||||||
if (refs.size() == 1) {
|
if (refs.size() == 1) {
|
||||||
String refName = Iterables.getOnlyElement(refs).getName();
|
String refName = Iterables.getOnlyElement(refs).getName();
|
||||||
@@ -142,7 +153,7 @@ class DefaultRefFilter {
|
|||||||
logger.atFinest().log("Filter out metadata ref %s", refName);
|
logger.atFinest().log("Filter out metadata ref %s", refName);
|
||||||
return ImmutableList.of();
|
return ImmutableList.of();
|
||||||
}
|
}
|
||||||
if (RefNames.isRefsChanges(refName)) {
|
if (RefNames.isRefsChanges(refName) && !opts.returnMostRecentRefChanges()) {
|
||||||
boolean isChangeRefVisisble = canSeeSingleChangeRef(refName);
|
boolean isChangeRefVisisble = canSeeSingleChangeRef(refName);
|
||||||
if (isChangeRefVisisble) {
|
if (isChangeRefVisisble) {
|
||||||
logger.atFinest().log("Change ref %s is visible", refName);
|
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);
|
logger.atFinest().log("visible refs = %s", visibleRefs);
|
||||||
return 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
|
* 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
|
* 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) {
|
} else if ((changeId = Change.Id.fromRef(refName)) != null) {
|
||||||
// This is a mere performance optimization. RefVisibilityControl could determine the
|
// This is a mere performance optimization. RefVisibilityControl could determine the
|
||||||
// visibility of these refs just fine. But instead, we use highly-optimized logic that
|
// 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) {
|
if (hasAccessDatabase) {
|
||||||
resultRefs.add(ref);
|
resultRefs.add(ref);
|
||||||
} else if (!visible(repo, changeId)) {
|
} else if (!visible(repo, changeId)) {
|
||||||
@@ -315,6 +348,11 @@ class DefaultRefFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean visible(Repository repo, Change.Id changeId) throws PermissionBackendException {
|
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 (visibleChanges == null) {
|
||||||
if (changeCache == null) {
|
if (changeCache == null) {
|
||||||
visibleChanges = visibleChangesByScan(repo);
|
visibleChanges = visibleChangesByScan(repo);
|
||||||
@@ -323,7 +361,7 @@ class DefaultRefFilter {
|
|||||||
}
|
}
|
||||||
logger.atFinest().log("Visible changes: %s", visibleChanges.keySet());
|
logger.atFinest().log("Visible changes: %s", visibleChanges.keySet());
|
||||||
}
|
}
|
||||||
return visibleChanges.containsKey(changeId);
|
return visibleChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean visibleEdit(Repository repo, String name) throws PermissionBackendException {
|
private boolean visibleEdit(Repository repo, String name) throws PermissionBackendException {
|
||||||
@@ -340,15 +378,11 @@ class DefaultRefFilter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize visibleChanges if it wasn't initialized yet.
|
if (visible(repo, id)) {
|
||||||
if (visibleChanges == null) {
|
|
||||||
visible(repo, id);
|
|
||||||
}
|
|
||||||
if (visibleChanges.containsKey(id)) {
|
|
||||||
try {
|
try {
|
||||||
// Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
|
// Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
|
||||||
permissionBackendForProject
|
permissionBackendForProject
|
||||||
.ref(visibleChanges.get(id).branch())
|
.ref(visibleChanges(repo).get(id).getChange().getDest().branch())
|
||||||
.check(RefPermission.READ_PRIVATE_CHANGES);
|
.check(RefPermission.READ_PRIVATE_CHANGES);
|
||||||
logger.atFinest().log("Foreign change edit ref is visible: %s", name);
|
logger.atFinest().log("Foreign change edit ref is visible: %s", name);
|
||||||
return true;
|
return true;
|
||||||
@@ -362,17 +396,17 @@ class DefaultRefFilter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Change.Id, BranchNameKey> visibleChangesBySearch() throws PermissionBackendException {
|
private Map<Change.Id, ChangeNotes> visibleChangesBySearch() throws PermissionBackendException {
|
||||||
Project.NameKey project = projectState.getNameKey();
|
Project.NameKey project = projectState.getNameKey();
|
||||||
try {
|
try {
|
||||||
Map<Change.Id, BranchNameKey> visibleChanges = new HashMap<>();
|
Map<Change.Id, ChangeNotes> visibleChanges = new HashMap<>();
|
||||||
for (ChangeData cd : changeCache.getChangeData(project)) {
|
for (ChangeData cd : changeCache.getChangeData(project)) {
|
||||||
if (!projectState.statePermitsRead()) {
|
if (!projectState.statePermitsRead()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
permissionBackendForProject.change(cd).check(ChangePermission.READ);
|
permissionBackendForProject.change(cd).check(ChangePermission.READ);
|
||||||
visibleChanges.put(cd.getId(), cd.change().getDest());
|
visibleChanges.put(cd.getId(), cd.notes());
|
||||||
} catch (AuthException e) {
|
} catch (AuthException e) {
|
||||||
// Do nothing.
|
// 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 {
|
throws PermissionBackendException {
|
||||||
Project.NameKey p = projectState.getNameKey();
|
Project.NameKey p = projectState.getNameKey();
|
||||||
ImmutableList<ChangeNotesResult> changes;
|
ImmutableList<ChangeNotesResult> changes;
|
||||||
@@ -397,11 +431,11 @@ class DefaultRefFilter {
|
|||||||
return Collections.emptyMap();
|
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) {
|
for (ChangeNotesResult notesResult : changes) {
|
||||||
ChangeNotes notes = toNotes(notesResult);
|
ChangeNotes notes = toNotes(notesResult);
|
||||||
if (notes != null) {
|
if (notes != null) {
|
||||||
result.put(notes.getChangeId(), notes.getChange().getDest());
|
result.put(notes.getChangeId(), notes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -329,6 +329,13 @@ public abstract class PermissionBackend {
|
|||||||
/** Remove all NoteDb refs (refs/changes/*, refs/users/*, edit refs) from the result. */
|
/** Remove all NoteDb refs (refs/changes/*, refs/users/*, edit refs) from the result. */
|
||||||
public abstract boolean filterMeta();
|
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
|
* Select only refs with names matching prefixes per {@link
|
||||||
* org.eclipse.jgit.lib.RefDatabase#getRefsByPrefix}.
|
* org.eclipse.jgit.lib.RefDatabase#getRefsByPrefix}.
|
||||||
@@ -340,6 +347,7 @@ public abstract class PermissionBackend {
|
|||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
|
return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
|
||||||
.setFilterMeta(false)
|
.setFilterMeta(false)
|
||||||
|
.setReturnMostRecentRefChanges(false)
|
||||||
.setPrefixes(Collections.singletonList(""));
|
.setPrefixes(Collections.singletonList(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +355,8 @@ public abstract class PermissionBackend {
|
|||||||
public abstract static class Builder {
|
public abstract static class Builder {
|
||||||
public abstract Builder setFilterMeta(boolean val);
|
public abstract Builder setFilterMeta(boolean val);
|
||||||
|
|
||||||
|
public abstract Builder setReturnMostRecentRefChanges(boolean val);
|
||||||
|
|
||||||
public abstract Builder setPrefixes(List<String> prefixes);
|
public abstract Builder setPrefixes(List<String> prefixes);
|
||||||
|
|
||||||
public abstract RefFilterOptions build();
|
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
|
@Test
|
||||||
public void fetchSingleChangeWithoutIndexAccess() throws Exception {
|
public void fetchSingleChangeWithoutIndexAccess() throws Exception {
|
||||||
PushOneCommit.Result change = createChange();
|
PushOneCommit.Result change = createChange();
|
||||||
|
|||||||
Reference in New Issue
Block a user