| // 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.acceptance.testsuite.change; | |
| import static com.google.common.base.Preconditions.checkState; | |
| import static com.google.common.collect.ImmutableList.toImmutableList; | |
| import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Streams; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.entities.Account; | |
| import com.google.gerrit.entities.Change; | |
| import com.google.gerrit.entities.PatchSet; | |
| import com.google.gerrit.entities.Project; | |
| import com.google.gerrit.entities.RefNames; | |
| import com.google.gerrit.exceptions.StorageException; | |
| import com.google.gerrit.extensions.restapi.BadRequestException; | |
| import com.google.gerrit.extensions.restapi.RestApiException; | |
| import com.google.gerrit.server.ChangeUtil; | |
| import com.google.gerrit.server.GerritPersonIdent; | |
| import com.google.gerrit.server.IdentifiedUser; | |
| import com.google.gerrit.server.account.AccountResolver; | |
| import com.google.gerrit.server.change.ChangeFinder; | |
| import com.google.gerrit.server.change.ChangeInserter; | |
| import com.google.gerrit.server.change.PatchSetInserter; | |
| import com.google.gerrit.server.edit.tree.TreeCreator; | |
| import com.google.gerrit.server.edit.tree.TreeModification; | |
| import com.google.gerrit.server.git.GitRepositoryManager; | |
| import com.google.gerrit.server.notedb.ChangeNotes; | |
| import com.google.gerrit.server.notedb.Sequences; | |
| import com.google.gerrit.server.project.ProjectCache; | |
| import com.google.gerrit.server.update.BatchUpdate; | |
| import com.google.gerrit.server.update.UpdateException; | |
| import com.google.gerrit.server.update.context.RefUpdateContext; | |
| import com.google.gerrit.server.util.CommitMessageUtil; | |
| import com.google.gerrit.server.util.time.TimeUtil; | |
| import com.google.inject.Inject; | |
| import java.io.IOException; | |
| import java.time.Instant; | |
| import java.util.Arrays; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.lib.AnyObjectId; | |
| import org.eclipse.jgit.lib.CommitBuilder; | |
| import org.eclipse.jgit.lib.Config; | |
| 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.Repository; | |
| import org.eclipse.jgit.merge.MergeStrategy; | |
| import org.eclipse.jgit.merge.Merger; | |
| import org.eclipse.jgit.revwalk.RevCommit; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| import org.eclipse.jgit.util.ChangeIdUtil; | |
| /** | |
| * The implementation of {@link ChangeOperations}. | |
| * | |
| * <p>There is only one implementation of {@link ChangeOperations}. Nevertheless, we keep the | |
| * separation between interface and implementation to enhance clarity. | |
| */ | |
| public class ChangeOperationsImpl implements ChangeOperations { | |
| private final Sequences seq; | |
| private final ChangeInserter.Factory changeInserterFactory; | |
| private final PatchSetInserter.Factory patchsetInserterFactory; | |
| private final GitRepositoryManager repositoryManager; | |
| private final AccountResolver resolver; | |
| private final IdentifiedUser.GenericFactory userFactory; | |
| private final PersonIdent serverIdent; | |
| private final BatchUpdate.Factory batchUpdateFactory; | |
| private final ProjectCache projectCache; | |
| private final ChangeFinder changeFinder; | |
| private final PerPatchsetOperationsImpl.Factory perPatchsetOperationsFactory; | |
| private final PerCommentOperationsImpl.Factory perCommentOperationsFactory; | |
| private final PerDraftCommentOperationsImpl.Factory perDraftCommentOperationsFactory; | |
| private final PerRobotCommentOperationsImpl.Factory perRobotCommentOperationsFactory; | |
| @Inject | |
| public ChangeOperationsImpl( | |
| Sequences seq, | |
| ChangeInserter.Factory changeInserterFactory, | |
| PatchSetInserter.Factory patchsetInserterFactory, | |
| GitRepositoryManager repositoryManager, | |
| AccountResolver resolver, | |
| IdentifiedUser.GenericFactory userFactory, | |
| @GerritPersonIdent PersonIdent serverIdent, | |
| BatchUpdate.Factory batchUpdateFactory, | |
| ProjectCache projectCache, | |
| ChangeFinder changeFinder, | |
| PerPatchsetOperationsImpl.Factory perPatchsetOperationsFactory, | |
| PerCommentOperationsImpl.Factory perCommentOperationsFactory, | |
| PerDraftCommentOperationsImpl.Factory perDraftCommentOperationsFactory, | |
| PerRobotCommentOperationsImpl.Factory perRobotCommentOperationsFactory) { | |
| this.seq = seq; | |
| this.changeInserterFactory = changeInserterFactory; | |
| this.patchsetInserterFactory = patchsetInserterFactory; | |
| this.repositoryManager = repositoryManager; | |
| this.resolver = resolver; | |
| this.userFactory = userFactory; | |
| this.serverIdent = serverIdent; | |
| this.batchUpdateFactory = batchUpdateFactory; | |
| this.projectCache = projectCache; | |
| this.changeFinder = changeFinder; | |
| this.perPatchsetOperationsFactory = perPatchsetOperationsFactory; | |
| this.perCommentOperationsFactory = perCommentOperationsFactory; | |
| this.perDraftCommentOperationsFactory = perDraftCommentOperationsFactory; | |
| this.perRobotCommentOperationsFactory = perRobotCommentOperationsFactory; | |
| } | |
| @Override | |
| public PerChangeOperations change(Change.Id changeId) { | |
| return new PerChangeOperationsImpl(changeId); | |
| } | |
| @Override | |
| public TestChangeCreation.Builder newChange() { | |
| return TestChangeCreation.builder(this::createChange); | |
| } | |
| private Change.Id createChange(TestChangeCreation changeCreation) throws Exception { | |
| try (RefUpdateContext ctx = openTestRefUpdateContext()) { | |
| Change.Id changeId = Change.id(seq.nextChangeId()); | |
| Project.NameKey project = getTargetProject(changeCreation); | |
| try (Repository repository = repositoryManager.openRepository(project); | |
| ObjectInserter objectInserter = repository.newObjectInserter(); | |
| RevWalk revWalk = new RevWalk(objectInserter.newReader())) { | |
| Instant now = TimeUtil.now(); | |
| IdentifiedUser changeOwner = getChangeOwner(changeCreation); | |
| PersonIdent author = getAuthorIdent(now, changeCreation); | |
| PersonIdent committer = getCommitterIdent(now, changeCreation); | |
| ObjectId commitId = | |
| createCommit(repository, revWalk, objectInserter, changeCreation, author, committer); | |
| String refName = RefNames.fullName(changeCreation.branch()); | |
| ChangeInserter inserter = getChangeInserter(changeId, refName, commitId); | |
| inserter.setGroups(getGroups(changeCreation)); | |
| changeCreation.topic().ifPresent(t -> inserter.setTopic(t)); | |
| inserter.setApprovals(changeCreation.approvals()); | |
| try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) { | |
| batchUpdate.setRepository(repository, revWalk, objectInserter); | |
| batchUpdate.insertChange(inserter); | |
| batchUpdate.execute(); | |
| } | |
| return changeId; | |
| } | |
| } | |
| } | |
| private Project.NameKey getTargetProject(TestChangeCreation changeCreation) { | |
| if (changeCreation.project().isPresent()) { | |
| return changeCreation.project().get(); | |
| } | |
| return getArbitraryProject(); | |
| } | |
| private Project.NameKey getArbitraryProject() { | |
| Project.NameKey allProjectsName = projectCache.getAllProjects().getNameKey(); | |
| Project.NameKey allUsersName = projectCache.getAllUsers().getNameKey(); | |
| Optional<Project.NameKey> arbitraryProject = | |
| projectCache.all().stream() | |
| .filter( | |
| name -> | |
| !Objects.equals(name, allProjectsName) && !Objects.equals(name, allUsersName)) | |
| .findFirst(); | |
| checkState( | |
| arbitraryProject.isPresent(), | |
| "At least one repository must be available on the Gerrit server"); | |
| return arbitraryProject.get(); | |
| } | |
| private IdentifiedUser getChangeOwner(TestChangeCreation changeCreation) | |
| throws IOException, ConfigInvalidException { | |
| if (changeCreation.owner().isPresent()) { | |
| return userFactory.create(changeCreation.owner().get()); | |
| } | |
| return getArbitraryUser(); | |
| } | |
| private PersonIdent getAuthorIdent(Instant when, TestChangeCreation changeCreation) | |
| throws IOException, ConfigInvalidException { | |
| if (changeCreation.authorIdent().isPresent()) { | |
| return new PersonIdent(changeCreation.authorIdent().get(), when); | |
| } | |
| return (changeCreation.author().isPresent() | |
| ? userFactory.create(changeCreation.author().get()) | |
| : getChangeOwner(changeCreation)) | |
| .newCommitterIdent(when, serverIdent.getZoneId()); | |
| } | |
| private PersonIdent getCommitterIdent(Instant when, TestChangeCreation changeCreation) | |
| throws IOException, ConfigInvalidException { | |
| if (changeCreation.committerIdent().isPresent()) { | |
| return new PersonIdent(changeCreation.committerIdent().get(), when); | |
| } | |
| return (changeCreation.committer().isPresent() | |
| ? userFactory.create(changeCreation.committer().get()) | |
| : getChangeOwner(changeCreation)) | |
| .newCommitterIdent(when, serverIdent.getZoneId()); | |
| } | |
| private IdentifiedUser getArbitraryUser() throws ConfigInvalidException, IOException { | |
| ImmutableSet<Account.Id> foundAccounts = resolver.resolveIgnoreVisibility("").asIdSet(); | |
| checkState( | |
| !foundAccounts.isEmpty(), | |
| "At least one user account must be available on the Gerrit server"); | |
| return userFactory.create(foundAccounts.iterator().next()); | |
| } | |
| private ObjectId createCommit( | |
| Repository repository, | |
| RevWalk revWalk, | |
| ObjectInserter objectInserter, | |
| TestChangeCreation changeCreation, | |
| PersonIdent author, | |
| PersonIdent committer) | |
| throws IOException, BadRequestException { | |
| ImmutableList<ObjectId> parentCommits = getParentCommits(repository, revWalk, changeCreation); | |
| TreeCreator treeCreator = | |
| getTreeCreator(objectInserter, parentCommits, changeCreation.mergeStrategy()); | |
| ObjectId tree = createNewTree(repository, treeCreator, changeCreation.treeModifications()); | |
| String commitMessage = correctCommitMessage(changeCreation.commitMessage()); | |
| return createCommit(objectInserter, tree, parentCommits, author, committer, commitMessage); | |
| } | |
| private ImmutableList<String> getGroups(TestChangeCreation changeCreation) { | |
| return changeCreation | |
| .parents() | |
| .map(parents -> getGroups(parents)) | |
| .orElseGet(() -> ImmutableList.of()); | |
| } | |
| private ImmutableList<String> getGroups(ImmutableList<TestCommitIdentifier> parents) { | |
| return parents.stream() | |
| .map(parent -> getGroups(parent)) | |
| .flatMap(groups -> groups.stream()) | |
| .collect(toImmutableList()); | |
| } | |
| private ImmutableList<String> getGroups(TestCommitIdentifier parentCommit) { | |
| switch (parentCommit.getKind()) { | |
| case BRANCH: | |
| return ImmutableList.of(); | |
| case CHANGE_ID: | |
| return getGroupsFromChange(parentCommit.changeId()); | |
| case COMMIT_SHA_1: | |
| return ImmutableList.of(); | |
| case PATCHSET_ID: | |
| return getGroupsFromPatchset(parentCommit.patchsetId()); | |
| default: | |
| throw new IllegalStateException( | |
| String.format("No parent behavior implemented for %s.", parentCommit.getKind())); | |
| } | |
| } | |
| private ImmutableList<String> getGroupsFromChange(Change.Id changeId) { | |
| Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId); | |
| if (changeNotes.isPresent() && changeNotes.get().getChange().isClosed()) { | |
| return ImmutableList.of(); | |
| } | |
| return changeNotes | |
| .map(ChangeNotes::getCurrentPatchSet) | |
| .map(PatchSet::groups) | |
| .orElseThrow( | |
| () -> | |
| new IllegalStateException( | |
| String.format( | |
| "Change %s not found and hence can't be used as parent.", changeId))); | |
| } | |
| private ImmutableList<String> getGroupsFromPatchset(PatchSet.Id patchsetId) { | |
| Optional<ChangeNotes> changeNotes = changeFinder.findOne(patchsetId.changeId()); | |
| if (changeNotes.isPresent() && changeNotes.get().getChange().isClosed()) { | |
| return ImmutableList.of(); | |
| } | |
| return changeNotes | |
| .map(ChangeNotes::getPatchSets) | |
| .map(patchsets -> patchsets.get(patchsetId)) | |
| .map(PatchSet::groups) | |
| .orElseThrow( | |
| () -> | |
| new IllegalStateException( | |
| String.format( | |
| "Patchset %s not found and hence can't be used as parent.", patchsetId))); | |
| } | |
| private ImmutableList<ObjectId> getParentCommits( | |
| Repository repository, RevWalk revWalk, TestChangeCreation changeCreation) { | |
| return changeCreation | |
| .parents() | |
| .map(parents -> resolveParents(repository, revWalk, parents)) | |
| .orElseGet(() -> asImmutableList(getTip(repository, changeCreation.branch()))); | |
| } | |
| private ImmutableList<ObjectId> resolveParents( | |
| Repository repository, RevWalk revWalk, ImmutableList<TestCommitIdentifier> parents) { | |
| return parents.stream() | |
| .map(parent -> resolveCommit(repository, revWalk, parent)) | |
| .collect(toImmutableList()); | |
| } | |
| private ObjectId resolveCommit( | |
| Repository repository, RevWalk revWalk, TestCommitIdentifier parentCommit) { | |
| switch (parentCommit.getKind()) { | |
| case BRANCH: | |
| return resolveBranchTip(repository, parentCommit.branch()); | |
| case CHANGE_ID: | |
| return resolveChange(parentCommit.changeId()); | |
| case COMMIT_SHA_1: | |
| return resolveCommitFromSha1(revWalk, parentCommit.commitSha1()); | |
| case PATCHSET_ID: | |
| return resolvePatchset(parentCommit.patchsetId()); | |
| default: | |
| throw new IllegalStateException( | |
| String.format("No parent behavior implemented for %s.", parentCommit.getKind())); | |
| } | |
| } | |
| private static ObjectId resolveBranchTip(Repository repository, String branchName) { | |
| return getTip(repository, branchName) | |
| .orElseThrow( | |
| () -> | |
| new IllegalStateException( | |
| String.format( | |
| "Tip of branch %s not found and hence can't be used as parent.", | |
| branchName))); | |
| } | |
| private static Optional<ObjectId> getTip(Repository repository, String branch) { | |
| try { | |
| Optional<Ref> ref = Optional.ofNullable(repository.findRef(branch)); | |
| return ref.map(Ref::getObjectId); | |
| } catch (IOException e) { | |
| throw new StorageException(e); | |
| } | |
| } | |
| private ObjectId resolveChange(Change.Id changeId) { | |
| Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId); | |
| return changeNotes | |
| .map(ChangeNotes::getCurrentPatchSet) | |
| .map(PatchSet::commitId) | |
| .orElseThrow( | |
| () -> | |
| new IllegalStateException( | |
| String.format( | |
| "Change %s not found and hence can't be used as parent.", changeId))); | |
| } | |
| private static RevCommit resolveCommitFromSha1(RevWalk revWalk, ObjectId commitSha1) { | |
| try { | |
| return revWalk.parseCommit(commitSha1); | |
| } catch (Exception e) { | |
| throw new IllegalStateException( | |
| String.format("Commit %s not found and hence can't be used as parent/base.", commitSha1), | |
| e); | |
| } | |
| } | |
| private ObjectId resolvePatchset(PatchSet.Id patchsetId) { | |
| Optional<ChangeNotes> changeNotes = changeFinder.findOne(patchsetId.changeId()); | |
| return changeNotes | |
| .map(ChangeNotes::getPatchSets) | |
| .map(patchsets -> patchsets.get(patchsetId)) | |
| .map(PatchSet::commitId) | |
| .orElseThrow( | |
| () -> | |
| new IllegalStateException( | |
| String.format( | |
| "Patchset %s not found and hence can't be used as parent.", patchsetId))); | |
| } | |
| private static <T> ImmutableList<T> asImmutableList(Optional<T> value) { | |
| return Streams.stream(value).collect(toImmutableList()); | |
| } | |
| private static TreeCreator getTreeCreator( | |
| RevWalk revWalk, ObjectId customBaseCommit, ImmutableList<ObjectId> parentCommits) { | |
| RevCommit commit = resolveCommitFromSha1(revWalk, customBaseCommit); | |
| // Use actual parents; relevant for example when a file is restored (-> | |
| // RestoreFileModification). | |
| return TreeCreator.basedOnTree(commit.getTree(), parentCommits); | |
| } | |
| private static TreeCreator getTreeCreator( | |
| ObjectInserter objectInserter, | |
| ImmutableList<ObjectId> parentCommits, | |
| MergeStrategy mergeStrategy) { | |
| if (parentCommits.isEmpty()) { | |
| return TreeCreator.basedOnEmptyTree(); | |
| } | |
| ObjectId baseTreeId = merge(objectInserter, parentCommits, mergeStrategy); | |
| return TreeCreator.basedOnTree(baseTreeId, parentCommits); | |
| } | |
| private static ObjectId merge( | |
| ObjectInserter objectInserter, | |
| ImmutableList<ObjectId> parentCommits, | |
| MergeStrategy mergeStrategy) { | |
| try { | |
| Merger merger = mergeStrategy.newMerger(objectInserter, new Config()); | |
| boolean mergeSuccessful = merger.merge(parentCommits.toArray(new AnyObjectId[0])); | |
| if (!mergeSuccessful) { | |
| throw new IllegalStateException( | |
| "Conflicts encountered while merging the specified parents. Use" | |
| + " mergeOfButBaseOnFirst() instead to avoid these conflicts and define any" | |
| + " other desired file contents with file().content()."); | |
| } | |
| return merger.getResultTreeId(); | |
| } catch (IOException e) { | |
| throw new IllegalStateException( | |
| "Creating the merge commits of the specified parents failed for an unknown reason.", e); | |
| } | |
| } | |
| private static ObjectId createNewTree( | |
| Repository repository, | |
| TreeCreator treeCreator, | |
| ImmutableList<TreeModification> treeModifications) | |
| throws IOException { | |
| treeCreator.addTreeModifications(treeModifications); | |
| return treeCreator.createNewTreeAndGetId(repository); | |
| } | |
| private String correctCommitMessage(String desiredCommitMessage) throws BadRequestException { | |
| String commitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(desiredCommitMessage); | |
| if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) { | |
| ObjectId id = CommitMessageUtil.generateChangeId(); | |
| commitMessage = ChangeIdUtil.insertId(commitMessage, id); | |
| } | |
| return commitMessage; | |
| } | |
| private ObjectId createCommit( | |
| ObjectInserter objectInserter, | |
| ObjectId tree, | |
| ImmutableList<ObjectId> parentCommitIds, | |
| PersonIdent author, | |
| PersonIdent committer, | |
| String commitMessage) | |
| throws IOException { | |
| CommitBuilder builder = new CommitBuilder(); | |
| builder.setTreeId(tree); | |
| builder.setParentIds(parentCommitIds); | |
| builder.setAuthor(author); | |
| builder.setCommitter(committer); | |
| builder.setMessage(commitMessage); | |
| ObjectId newCommitId = objectInserter.insert(builder); | |
| objectInserter.flush(); | |
| return newCommitId; | |
| } | |
| private ChangeInserter getChangeInserter(Change.Id changeId, String refName, ObjectId commitId) { | |
| ChangeInserter inserter = changeInserterFactory.create(changeId, commitId, refName); | |
| inserter.setMessage(String.format("Uploaded patchset %d.", inserter.getPatchSetId().get())); | |
| return inserter; | |
| } | |
| private class PerChangeOperationsImpl implements PerChangeOperations { | |
| private final Change.Id changeId; | |
| public PerChangeOperationsImpl(Change.Id changeId) { | |
| this.changeId = changeId; | |
| } | |
| @Override | |
| public boolean exists() { | |
| return changeFinder.findOne(changeId).isPresent(); | |
| } | |
| @Override | |
| public TestChange get() { | |
| return toTestChange(getChangeNotes().getChange()); | |
| } | |
| private ChangeNotes getChangeNotes() { | |
| Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId); | |
| checkState(changeNotes.isPresent(), "Tried to get non-existing test change."); | |
| return changeNotes.get(); | |
| } | |
| private TestChange toTestChange(Change change) { | |
| return TestChange.builder() | |
| .numericChangeId(change.getId()) | |
| .changeId(change.getKey().get()) | |
| .build(); | |
| } | |
| @Override | |
| public TestPatchsetCreation.Builder newPatchset() { | |
| return TestPatchsetCreation.builder(this::createPatchset); | |
| } | |
| private PatchSet.Id createPatchset(TestPatchsetCreation patchsetCreation) | |
| throws IOException, RestApiException, UpdateException { | |
| try (RefUpdateContext ctx = openTestRefUpdateContext()) { | |
| ChangeNotes changeNotes = getChangeNotes(); | |
| Project.NameKey project = changeNotes.getProjectName(); | |
| try (Repository repository = repositoryManager.openRepository(project); | |
| ObjectInserter objectInserter = repository.newObjectInserter(); | |
| RevWalk revWalk = new RevWalk(objectInserter.newReader())) { | |
| Instant now = TimeUtil.now(); | |
| PersonIdent authorIdent = getAuthorIdent(now, patchsetCreation); | |
| PersonIdent committerIdent = getCommitterIdent(now, patchsetCreation); | |
| ObjectId newPatchsetCommit = | |
| createPatchsetCommit( | |
| repository, | |
| revWalk, | |
| objectInserter, | |
| changeNotes, | |
| patchsetCreation, | |
| authorIdent, | |
| committerIdent, | |
| now); | |
| PatchSet.Id patchsetId = | |
| ChangeUtil.nextPatchSetId(repository, changeNotes.getCurrentPatchSet().id()); | |
| PatchSetInserter patchSetInserter = | |
| getPatchSetInserter(changeNotes, newPatchsetCommit, patchsetId); | |
| Account.Id uploaderId = | |
| patchsetCreation.uploader().orElse(changeNotes.getChange().getOwner()); | |
| IdentifiedUser uploader = userFactory.create(uploaderId); | |
| try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, uploader, now)) { | |
| batchUpdate.setRepository(repository, revWalk, objectInserter); | |
| batchUpdate.addOp(changeId, patchSetInserter); | |
| batchUpdate.execute(); | |
| } | |
| return patchsetId; | |
| } | |
| } | |
| } | |
| @Nullable | |
| private PersonIdent getAuthorIdent(Instant when, TestPatchsetCreation patchsetCreation) { | |
| if (patchsetCreation.authorIdent().isPresent()) { | |
| return new PersonIdent(patchsetCreation.authorIdent().get(), when); | |
| } | |
| if (patchsetCreation.author().isPresent()) { | |
| return userFactory | |
| .create(patchsetCreation.author().get()) | |
| .newCommitterIdent(when, serverIdent.getZoneId()); | |
| } | |
| return null; | |
| } | |
| @Nullable | |
| private PersonIdent getCommitterIdent(Instant when, TestPatchsetCreation patchsetCreation) { | |
| if (patchsetCreation.committerIdent().isPresent()) { | |
| return new PersonIdent(patchsetCreation.committerIdent().get(), when); | |
| } | |
| if (patchsetCreation.committer().isPresent()) { | |
| return userFactory | |
| .create(patchsetCreation.committer().get()) | |
| .newCommitterIdent(when, serverIdent.getZoneId()); | |
| } | |
| return null; | |
| } | |
| private ObjectId createPatchsetCommit( | |
| Repository repository, | |
| RevWalk revWalk, | |
| ObjectInserter objectInserter, | |
| ChangeNotes changeNotes, | |
| TestPatchsetCreation patchsetCreation, | |
| @Nullable PersonIdent author, | |
| @Nullable PersonIdent committer, | |
| Instant now) | |
| throws IOException, BadRequestException { | |
| ObjectId oldPatchsetCommitId = changeNotes.getCurrentPatchSet().commitId(); | |
| RevCommit oldPatchsetCommit = repository.parseCommit(oldPatchsetCommitId); | |
| ImmutableList<ObjectId> parentCommitIds = | |
| getParents(repository, revWalk, patchsetCreation, oldPatchsetCommit); | |
| TreeCreator treeCreator = getTreeCreator(revWalk, oldPatchsetCommit, parentCommitIds); | |
| ObjectId tree = createNewTree(repository, treeCreator, patchsetCreation.treeModifications()); | |
| String commitMessage = | |
| correctCommitMessage( | |
| changeNotes.getChange().getKey().get(), | |
| patchsetCreation.commitMessage().orElseGet(oldPatchsetCommit::getFullMessage)); | |
| return createCommit( | |
| objectInserter, | |
| tree, | |
| parentCommitIds, | |
| Optional.ofNullable(author).orElse(getAuthor(oldPatchsetCommit)), | |
| Optional.ofNullable(committer).orElse(getCommitter(oldPatchsetCommit, now)), | |
| commitMessage); | |
| } | |
| private String correctCommitMessage(String oldChangeId, String desiredCommitMessage) | |
| throws BadRequestException { | |
| String commitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(desiredCommitMessage); | |
| // Remove initial 'I' and treat the rest as ObjectId. This is not the cleanest approach but | |
| // unfortunately, we don't seem to have other utility code which takes the string-based | |
| // change-id and ensures that it is part of the commit message. | |
| ObjectId id = ObjectId.fromString(oldChangeId.substring(1)); | |
| commitMessage = ChangeIdUtil.insertId(commitMessage, id, false); | |
| return commitMessage; | |
| } | |
| private PersonIdent getAuthor(RevCommit oldPatchsetCommit) { | |
| return Optional.ofNullable(oldPatchsetCommit.getAuthorIdent()).orElse(serverIdent); | |
| } | |
| private PersonIdent getCommitter(RevCommit oldPatchsetCommit, Instant now) { | |
| PersonIdent oldPatchsetCommitter = | |
| Optional.ofNullable(oldPatchsetCommit.getCommitterIdent()).orElse(serverIdent); | |
| if (asSeconds(now) == asSeconds(oldPatchsetCommitter.getWhenAsInstant())) { | |
| /* We need to ensure that the resulting commit SHA-1 is different from the old patchset. | |
| * In real situations, this automatically happens as two patchsets won't have exactly the | |
| * same commit timestamp even when the tree and commit message are the same. In tests, | |
| * we can easily end up with the same timestamp as Git uses second precision for timestamps. | |
| * We could of course require that tests must use TestTimeUtil#setClockStep but | |
| * that would be an unnecessary nuisance for test writers. Hence, go with a simple solution | |
| * here and simply add a second. */ | |
| now = now.plusSeconds(1); | |
| } | |
| return new PersonIdent(oldPatchsetCommitter, now); | |
| } | |
| private long asSeconds(Instant date) { | |
| return date.getEpochSecond(); | |
| } | |
| private ImmutableList<ObjectId> getParents( | |
| Repository repository, | |
| RevWalk revWalk, | |
| TestPatchsetCreation patchsetCreation, | |
| RevCommit oldPatchsetCommit) { | |
| return patchsetCreation | |
| .parents() | |
| .map(parents -> resolveParents(repository, revWalk, parents)) | |
| .orElseGet( | |
| () -> Arrays.stream(oldPatchsetCommit.getParents()).collect(toImmutableList())); | |
| } | |
| private PatchSetInserter getPatchSetInserter( | |
| ChangeNotes changeNotes, ObjectId newPatchsetCommit, PatchSet.Id patchsetId) { | |
| PatchSetInserter patchSetInserter = | |
| patchsetInserterFactory.create(changeNotes, patchsetId, newPatchsetCommit); | |
| patchSetInserter.setCheckAddPatchSetPermission(false); | |
| patchSetInserter.setMessage(String.format("Uploaded patchset %d.", patchsetId.get())); | |
| return patchSetInserter; | |
| } | |
| @Override | |
| public PerPatchsetOperations patchset(PatchSet.Id patchsetId) { | |
| return perPatchsetOperationsFactory.create(getChangeNotes(), patchsetId); | |
| } | |
| @Override | |
| public PerPatchsetOperations currentPatchset() { | |
| ChangeNotes changeNotes = getChangeNotes(); | |
| return perPatchsetOperationsFactory.create( | |
| changeNotes, changeNotes.getChange().currentPatchSetId()); | |
| } | |
| @Override | |
| public PerCommentOperations comment(String commentUuid) { | |
| ChangeNotes changeNotes = getChangeNotes(); | |
| return perCommentOperationsFactory.create(changeNotes, commentUuid); | |
| } | |
| @Override | |
| public PerDraftCommentOperations draftComment(String commentUuid) { | |
| ChangeNotes changeNotes = getChangeNotes(); | |
| return perDraftCommentOperationsFactory.create(changeNotes, commentUuid); | |
| } | |
| @Override | |
| public PerRobotCommentOperations robotComment(String commentUuid) { | |
| ChangeNotes changeNotes = getChangeNotes(); | |
| return perRobotCommentOperationsFactory.create(changeNotes, commentUuid); | |
| } | |
| } | |
| } |