| // 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 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) -> {}); |
| clean.execute(); |
| |
| /* 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) -> {}); |
| clean.execute(); |
| |
| /* 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 */ |
| clean.execute(); |
| 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) -> {}); |
| |
| clean.execute(); |
| |
| /* 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) -> {}); |
| clean.execute(); |
| |
| 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); |
| } |
| 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); |
| 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; |
| } |
| } |
| } |