| // Copyright (C) 2020 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.server.git; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.entities.RefNames; | |
| import com.google.gerrit.git.RefUpdateUtil; | |
| import com.google.gerrit.server.config.AllUsersName; | |
| import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs; | |
| import com.google.gerrit.server.util.time.TimeUtil; | |
| import com.google.gerrit.testing.InMemoryRepositoryManager; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import org.eclipse.jgit.lib.BatchRefUpdate; | |
| import org.eclipse.jgit.lib.CommitBuilder; | |
| import org.eclipse.jgit.lib.Constants; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.ObjectInserter; | |
| 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.eclipse.jgit.lib.TreeFormatter; | |
| import org.eclipse.jgit.revwalk.RevBlob; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| import org.eclipse.jgit.transport.ReceiveCommand; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| @RunWith(JUnit4.class) | |
| public class DeleteZombieCommentsRefsTest { | |
| private InMemoryRepositoryManager repoManager = new InMemoryRepositoryManager(); | |
| private Project.NameKey allUsersProject = Project.nameKey("All-Users"); | |
| @Test | |
| public void cleanZombieDraftsSmall() throws Exception { | |
| try (Repository usersRepo = repoManager.createRepository(allUsersProject)) { | |
| Ref ref1 = createRefWithNonEmptyTreeCommit(usersRepo, 1, 1000001); | |
| Ref ref2 = createRefWithEmptyTreeCommit(usersRepo, 1, 1000002); | |
| DeleteZombieCommentsRefs clean = | |
| new DeleteZombieCommentsRefs( | |
| new AllUsersName("All-Users"), repoManager, null, (msg) -> {}); | |
| int deletedDrafts = clean.execute(); | |
| assertThat(deletedDrafts).isEqualTo(1); | |
| /* Check that ref1 still exists, and ref2 is deleted */ | |
| assertThat(usersRepo.exactRef(ref1.getName())).isNotNull(); | |
| assertThat(usersRepo.exactRef(ref2.getName())).isNull(); | |
| } | |
| } | |
| @Test | |
| public void cleanZombieDraftsWithPercentage() throws Exception { | |
| try (Repository usersRepo = repoManager.createRepository(allUsersProject)) { | |
| Ref ref1 = createRefWithNonEmptyTreeCommit(usersRepo, 1005, 1000001); | |
| Ref ref2 = createRefWithEmptyTreeCommit(usersRepo, 1006, 1000002); | |
| Ref ref3 = createRefWithEmptyTreeCommit(usersRepo, 1060, 1000002); | |
| assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(3); | |
| int cleanupPercentage = 50; | |
| DeleteZombieCommentsRefs clean = | |
| new DeleteZombieCommentsRefs( | |
| new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {}); | |
| int deletedDrafts = clean.execute(); | |
| assertThat(deletedDrafts).isEqualTo(1); | |
| /* ref1 not deleted, ref2 deleted, ref3 not deleted because of the clean percentage */ | |
| assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(2); | |
| assertThat(usersRepo.exactRef(ref1.getName())).isNotNull(); | |
| assertThat(usersRepo.exactRef(ref2.getName())).isNull(); | |
| assertThat(usersRepo.exactRef(ref3.getName())).isNotNull(); | |
| /* Re-execute the cleanup and make sure nothing's changed */ | |
| deletedDrafts = clean.execute(); | |
| assertThat(deletedDrafts).isEqualTo(0); | |
| assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(2); | |
| assertThat(usersRepo.exactRef(ref1.getName())).isNotNull(); | |
| assertThat(usersRepo.exactRef(ref2.getName())).isNull(); | |
| assertThat(usersRepo.exactRef(ref3.getName())).isNotNull(); | |
| /* Increase the cleanup percentage */ | |
| cleanupPercentage = 70; | |
| clean = | |
| new DeleteZombieCommentsRefs( | |
| new AllUsersName("All-Users"), repoManager, cleanupPercentage, (msg) -> {}); | |
| deletedDrafts = clean.execute(); | |
| assertThat(deletedDrafts).isEqualTo(1); | |
| /* Now ref3 is deleted */ | |
| assertThat(usersRepo.getRefDatabase().getRefs()).hasSize(1); | |
| assertThat(usersRepo.exactRef(ref1.getName())).isNotNull(); | |
| assertThat(usersRepo.exactRef(ref2.getName())).isNull(); | |
| assertThat(usersRepo.exactRef(ref3.getName())).isNull(); | |
| } | |
| } | |
| @Test | |
| public void cleanZombieDraftsLarge() throws Exception { | |
| try (Repository usersRepo = repoManager.createRepository(allUsersProject)) { | |
| int goodRefsCnt = 5000; | |
| int zombieRefsCnt = 5000; | |
| int userIdGoodRefs = 1000001; | |
| int userIdBadRefs = 1000002; | |
| Ref nonEmptyBaseRef = createRefWithNonEmptyTreeCommit(usersRepo, 1, userIdGoodRefs); | |
| Ref emptyBaseRef = createRefWithEmptyTreeCommit(usersRepo, 1, userIdBadRefs); | |
| List<String> goodRefs = | |
| createNRefsOnCommit( | |
| usersRepo, nonEmptyBaseRef.getObjectId(), goodRefsCnt, userIdGoodRefs); | |
| List<String> badRefs = | |
| createNRefsOnCommit(usersRepo, emptyBaseRef.getObjectId(), zombieRefsCnt, userIdBadRefs); | |
| goodRefs.add(0, nonEmptyBaseRef.getName()); | |
| badRefs.add(0, emptyBaseRef.getName()); | |
| assertThat(usersRepo.getRefDatabase().getRefs().size()) | |
| .isEqualTo(goodRefs.size() + badRefs.size()); | |
| DeleteZombieCommentsRefs clean = | |
| new DeleteZombieCommentsRefs( | |
| new AllUsersName("All-Users"), repoManager, null, (msg) -> {}); | |
| int deletedDrafts = clean.execute(); | |
| assertThat(deletedDrafts).isEqualTo(5001); | |
| assertThat( | |
| usersRepo.getRefDatabase().getRefs().stream() | |
| .map(Ref::getName) | |
| .collect(toImmutableList())) | |
| .containsExactlyElementsIn(goodRefs); | |
| assertThat( | |
| usersRepo.getRefDatabase().getRefs().stream() | |
| .map(Ref::getName) | |
| .collect(toImmutableList())) | |
| .containsNoneIn(badRefs); | |
| } | |
| } | |
| private static List<String> createNRefsOnCommit( | |
| Repository usersRepo, ObjectId commitId, int n, int uuid) throws IOException { | |
| List<String> refNames = new ArrayList<>(); | |
| BatchRefUpdate bru = usersRepo.getRefDatabase().newBatchUpdate(); | |
| bru.setAtomic(true); | |
| for (int i = 2; i <= n + 1; i++) { | |
| String refName = getRefName(i, uuid); | |
| bru.addCommand( | |
| new ReceiveCommand(ObjectId.zeroId(), commitId, refName, ReceiveCommand.Type.CREATE)); | |
| refNames.add(refName); | |
| } | |
| testRefAction(() -> RefUpdateUtil.executeChecked(bru, usersRepo)); | |
| return refNames; | |
| } | |
| private static String getRefName(int changeId, int userId) { | |
| Change.Id cId = Change.id(changeId); | |
| Account.Id aId = Account.id(userId); | |
| return RefNames.refsDraftComments(cId, aId); | |
| } | |
| private static Ref createRefWithNonEmptyTreeCommit(Repository usersRepo, int changeId, int userId) | |
| throws IOException { | |
| try (RevWalk rw = new RevWalk(usersRepo)) { | |
| ObjectId fileObj = createBlob(usersRepo, String.format("file %d content", changeId)); | |
| ObjectId treeObj = | |
| createTree(usersRepo, rw.lookupBlob(fileObj), String.format("file%d.txt", changeId)); | |
| ObjectId commitObj = createCommit(usersRepo, treeObj, null); | |
| Ref refObj = createRef(usersRepo, commitObj, getRefName(changeId, userId)); | |
| return refObj; | |
| } | |
| } | |
| private static Ref createRefWithEmptyTreeCommit(Repository usersRepo, int changeId, int userId) | |
| throws IOException { | |
| ObjectId treeEmpty = createTree(usersRepo, null, ""); | |
| ObjectId commitObj = createCommit(usersRepo, treeEmpty, null); | |
| Ref refObj = createRef(usersRepo, commitObj, getRefName(changeId, userId)); | |
| return refObj; | |
| } | |
| private static Ref createRef(Repository repo, ObjectId commitId, String refName) | |
| throws IOException { | |
| RefUpdate update = repo.updateRef(refName); | |
| update.setNewObjectId(commitId); | |
| update.setForceUpdate(true); | |
| testRefAction(() -> update.update()); | |
| return repo.exactRef(refName); | |
| } | |
| private static ObjectId createCommit(Repository repo, ObjectId treeId, ObjectId parentCommit) | |
| throws IOException { | |
| try (ObjectInserter oi = repo.newObjectInserter()) { | |
| PersonIdent committer = | |
| new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.now()); | |
| CommitBuilder cb = new CommitBuilder(); | |
| cb.setTreeId(treeId); | |
| cb.setCommitter(committer); | |
| cb.setAuthor(committer); | |
| cb.setMessage("Test commit"); | |
| if (parentCommit != null) { | |
| cb.setParentIds(parentCommit); | |
| } | |
| ObjectId commitId = oi.insert(cb); | |
| oi.flush(); | |
| oi.close(); | |
| return commitId; | |
| } | |
| } | |
| private static ObjectId createTree(Repository repo, RevBlob blob, String blobName) | |
| throws IOException { | |
| try (ObjectInserter oi = repo.newObjectInserter()) { | |
| TreeFormatter formatter = new TreeFormatter(); | |
| if (blob != null) { | |
| formatter.append(blobName, blob); | |
| } | |
| ObjectId treeId = oi.insert(formatter); | |
| oi.flush(); | |
| oi.close(); | |
| return treeId; | |
| } | |
| } | |
| private static ObjectId createBlob(Repository repo, String content) throws IOException { | |
| try (ObjectInserter oi = repo.newObjectInserter()) { | |
| ObjectId blobId = oi.insert(Constants.OBJ_BLOB, content.getBytes(UTF_8)); | |
| oi.flush(); | |
| oi.close(); | |
| return blobId; | |
| } | |
| } | |
| } |