Merge changes from topic "gr-plugin-popup-to-ts"
* changes:
Move gr-plugin-popup to typescript
Rename files to preserve history
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 2d62608..07aab3b 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -30,6 +30,9 @@
import com.google.gerrit.acceptance.config.GlobalPluginConfigs;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.account.AccountOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
+import com.google.gerrit.acceptance.testsuite.change.ChangeOperationsImpl;
+import com.google.gerrit.acceptance.testsuite.change.PerPatchsetOperationsImpl;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -506,6 +509,8 @@
bind(GroupOperations.class).to(GroupOperationsImpl.class);
bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
bind(RequestScopeOperations.class).to(RequestScopeOperationsImpl.class);
+ bind(ChangeOperations.class).to(ChangeOperationsImpl.class);
+ factory(PerPatchsetOperationsImpl.Factory.class);
factory(PushOneCommit.Factory.class);
install(InProcessProtocol.module());
install(new NoSshModule());
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java
new file mode 100644
index 0000000..cc96d5b
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperations.java
@@ -0,0 +1,115 @@
+// 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 com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+
+/**
+ * An aggregation of operations on changes for test purposes.
+ *
+ * <p>To execute the operations, no Gerrit permissions are necessary.
+ *
+ * <p><strong>Note:</strong> This interface is not implemented using the REST or extension API.
+ * Hence, it cannot be used for testing those APIs.
+ */
+public interface ChangeOperations {
+
+ /**
+ * Starts the fluent chain for querying or modifying a change. Please see the methods of {@link
+ * PerChangeOperations} for details on possible operations.
+ *
+ * @return an aggregation of operations on a specific change
+ */
+ PerChangeOperations change(Change.Id changeId);
+
+ /**
+ * Starts the fluent chain to create a change. The returned builder can be used to specify the
+ * attributes of the new change. To create the change for real, {@link
+ * TestChangeCreation.Builder#create()} must be called.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * Change.Id createdChangeId = changeOperations
+ * .newChange()
+ * .file("file1")
+ * .content("Line 1\nLine2\n")
+ * .create();
+ * </pre>
+ *
+ * <p><strong>Note:</strong> There must be at least one existing user and repository.
+ *
+ * @return a builder to create the new change
+ */
+ TestChangeCreation.Builder newChange();
+
+ /** An aggregation of methods on a specific change. */
+ interface PerChangeOperations {
+
+ /**
+ * Checks whether the change exists.
+ *
+ * @return {@code true} if the change exists
+ */
+ boolean exists();
+
+ /**
+ * Retrieves the change.
+ *
+ * <p><strong>Note:</strong> This call will fail with an exception if the requested change
+ * doesn't exist. If you want to check for the existence of a change, use {@link #exists()}
+ * instead.
+ *
+ * @return the corresponding {@code TestChange}
+ */
+ TestChange get();
+
+ /**
+ * Starts the fluent chain to create a new patchset. The returned builder can be used to specify
+ * the attributes of the new patchset. To create the patchset for real, {@link
+ * TestPatchsetCreation.Builder#create()} must be called.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * PatchSet.Id createdPatchsetId = changeOperations
+ * .change(changeId)
+ * .newPatchset()
+ * .file("file1")
+ * .content("Line 1\nLine2\n")
+ * .create();
+ * </pre>
+ *
+ * @return builder to create a new patchset
+ */
+ TestPatchsetCreation.Builder newPatchset();
+
+ /**
+ * Starts the fluent chain for querying or modifying a patchset. Please see the methods of
+ * {@link PerPatchsetOperations} for details on possible operations.
+ *
+ * @return an aggregation of operations on a specific patchset
+ */
+ PerPatchsetOperations patchset(PatchSet.Id patchsetId);
+
+ /**
+ * Like {@link #patchset(PatchSet.Id)} but for the current patchset.
+ *
+ * @return an aggregation of operations on a specific patchset
+ */
+ PerPatchsetOperations currentPatchset();
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
new file mode 100644
index 0000000..3bd6475
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -0,0 +1,412 @@
+// 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 com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
+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.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.util.CommitMessageUtil;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Objects;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+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.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;
+
+ @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) {
+ 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;
+ }
+
+ @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 {
+ 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())) {
+ Timestamp now = TimeUtil.nowTs();
+ IdentifiedUser changeOwner = getChangeOwner(changeCreation);
+ PersonIdent authorAndCommitter =
+ changeOwner.newCommitterIdent(now, serverIdent.getTimeZone());
+ ObjectId commitId =
+ createCommit(repository, revWalk, objectInserter, changeCreation, authorAndCommitter);
+
+ String refName = RefNames.fullName(changeCreation.branch());
+ ChangeInserter inserter = getChangeInserter(changeId, refName, commitId);
+
+ 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 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 authorAndCommitter)
+ throws IOException, BadRequestException {
+ Optional<ObjectId> branchTip = getTip(repository, changeCreation.branch());
+
+ ObjectId tree =
+ createNewTree(
+ repository,
+ revWalk,
+ branchTip.orElse(ObjectId.zeroId()),
+ changeCreation.treeModifications());
+
+ String commitMessage = correctCommitMessage(changeCreation.commitMessage());
+
+ ImmutableList<ObjectId> parentCommitIds = Streams.stream(branchTip).collect(toImmutableList());
+ return createCommit(
+ objectInserter,
+ tree,
+ parentCommitIds,
+ authorAndCommitter,
+ authorAndCommitter,
+ commitMessage);
+ }
+
+ private Optional<ObjectId> getTip(Repository repository, String branch) throws IOException {
+ Optional<Ref> ref = Optional.ofNullable(repository.findRef(branch));
+ return ref.map(Ref::getObjectId);
+ }
+
+ private static ObjectId createNewTree(
+ Repository repository,
+ RevWalk revWalk,
+ ObjectId baseCommitId,
+ ImmutableList<TreeModification> treeModifications)
+ throws IOException {
+ TreeCreator treeCreator = getTreeCreator(revWalk, baseCommitId);
+ treeCreator.addTreeModifications(treeModifications);
+ return treeCreator.createNewTreeAndGetId(repository);
+ }
+
+ private static TreeCreator getTreeCreator(RevWalk revWalk, ObjectId baseCommitId)
+ throws IOException {
+ if (ObjectId.zeroId().equals(baseCommitId)) {
+ return TreeCreator.basedOnEmptyTree();
+ }
+ RevCommit baseCommit = revWalk.parseCommit(baseCommitId);
+ return TreeCreator.basedOn(baseCommit);
+ }
+
+ 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 {
+ ChangeNotes changeNotes = getChangeNotes();
+ Project.NameKey project = changeNotes.getProjectName();
+ try (Repository repository = repositoryManager.openRepository(project);
+ ObjectInserter objectInserter = repository.newObjectInserter();
+ RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+ Timestamp now = TimeUtil.nowTs();
+ ObjectId newPatchsetCommit =
+ createPatchsetCommit(
+ repository, revWalk, objectInserter, changeNotes, patchsetCreation, now);
+
+ PatchSet.Id patchsetId =
+ ChangeUtil.nextPatchSetId(repository, changeNotes.getCurrentPatchSet().id());
+ PatchSetInserter patchSetInserter =
+ getPatchSetInserter(changeNotes, newPatchsetCommit, patchsetId);
+
+ IdentifiedUser changeOwner = userFactory.create(changeNotes.getChange().getOwner());
+ try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
+ batchUpdate.setRepository(repository, revWalk, objectInserter);
+ batchUpdate.addOp(changeId, patchSetInserter);
+ batchUpdate.execute();
+ }
+ return patchsetId;
+ }
+ }
+
+ private ObjectId createPatchsetCommit(
+ Repository repository,
+ RevWalk revWalk,
+ ObjectInserter objectInserter,
+ ChangeNotes changeNotes,
+ TestPatchsetCreation patchsetCreation,
+ Timestamp now)
+ throws IOException {
+ ObjectId oldPatchsetCommitId = changeNotes.getCurrentPatchSet().commitId();
+ RevCommit oldPatchsetCommit = repository.parseCommit(oldPatchsetCommitId);
+
+ ObjectId tree =
+ createNewTree(
+ repository, revWalk, oldPatchsetCommitId, patchsetCreation.treeModifications());
+
+ String commitMessage = oldPatchsetCommit.getFullMessage();
+
+ ImmutableList<ObjectId> parentCommitIds = getParents(oldPatchsetCommit);
+ PersonIdent author = getAuthor(oldPatchsetCommit);
+ PersonIdent committer = getCommitter(oldPatchsetCommit, now);
+ return createCommit(objectInserter, tree, parentCommitIds, author, committer, commitMessage);
+ }
+
+ private PersonIdent getAuthor(RevCommit oldPatchsetCommit) {
+ return Optional.ofNullable(oldPatchsetCommit.getAuthorIdent()).orElse(serverIdent);
+ }
+
+ private PersonIdent getCommitter(RevCommit oldPatchsetCommit, Timestamp now) {
+ PersonIdent oldPatchsetCommitter =
+ Optional.ofNullable(oldPatchsetCommit.getCommitterIdent()).orElse(serverIdent);
+ if (asSeconds(now) == asSeconds(oldPatchsetCommitter.getWhen())) {
+ /* 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 = Timestamp.from(now.toInstant().plusSeconds(1));
+ }
+ return new PersonIdent(oldPatchsetCommitter, now);
+ }
+
+ private long asSeconds(Date date) {
+ return date.getTime() / 1000;
+ }
+
+ private ImmutableList<ObjectId> getParents(RevCommit oldPatchsetCommit) {
+ return Arrays.stream(oldPatchsetCommit.getParents())
+ .map(ObjectId::toObjectId)
+ .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());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java b/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java
new file mode 100644
index 0000000..bae8407
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/FileContentBuilder.java
@@ -0,0 +1,60 @@
+// 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.checkNotNull;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
+import com.google.gerrit.server.edit.tree.DeleteFileModification;
+import com.google.gerrit.server.edit.tree.RenameFileModification;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import java.util.function.Consumer;
+
+/** Builder to simplify file content specification. */
+public class FileContentBuilder<T> {
+ private final T builder;
+ private final String filePath;
+ private final Consumer<TreeModification> modificationToBuilderAdder;
+
+ FileContentBuilder(
+ T builder, String filePath, Consumer<TreeModification> modificationToBuilderAdder) {
+ checkNotNull(Strings.emptyToNull(filePath), "File path must not be null or empty.");
+ this.builder = builder;
+ this.filePath = filePath;
+ this.modificationToBuilderAdder = modificationToBuilderAdder;
+ }
+
+ /** Content of the file. Must not be empty. */
+ public T content(String content) {
+ checkNotNull(
+ Strings.emptyToNull(content),
+ "Empty file content is not supported. Adjust test API if necessary.");
+ modificationToBuilderAdder.accept(
+ new ChangeFileContentModification(filePath, RawInputUtil.create(content)));
+ return builder;
+ }
+
+ public T delete() {
+ modificationToBuilderAdder.accept(new DeleteFileModification(filePath));
+ return builder;
+ }
+
+ public T renameTo(String newFilePath) {
+ modificationToBuilderAdder.accept(new RenameFileModification(filePath, newFilePath));
+ return builder;
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java
new file mode 100644
index 0000000..c095551
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperations.java
@@ -0,0 +1,28 @@
+// 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;
+
+/** An aggregation of methods on a specific patchset. */
+public interface PerPatchsetOperations {
+
+ /**
+ * Retrieves the patchset.
+ *
+ * <p><strong>Note:</strong> This call will fail with an exception if the requested patchset
+ * doesn't exist.
+ *
+ * @return the corresponding {@code TestPatchset}
+ */
+ TestPatchset get();
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
new file mode 100644
index 0000000..8c9a495
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -0,0 +1,48 @@
+// 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 com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * The implementation of {@link PerPatchsetOperations}.
+ *
+ * <p>There is only one implementation of {@link PerPatchsetOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+public class PerPatchsetOperationsImpl implements PerPatchsetOperations {
+ private final ChangeNotes changeNotes;
+ private final PatchSet.Id patchsetId;
+
+ public interface Factory {
+ PerPatchsetOperationsImpl create(ChangeNotes changeNotes, PatchSet.Id patchsetId);
+ }
+
+ @Inject
+ private PerPatchsetOperationsImpl(
+ @Assisted ChangeNotes changeNotes, @Assisted PatchSet.Id patchsetId) {
+ this.changeNotes = changeNotes;
+ this.patchsetId = patchsetId;
+ }
+
+ @Override
+ public TestPatchset get() {
+ PatchSet patchset = changeNotes.getPatchSets().get(patchsetId);
+ return TestPatchset.builder().patchsetId(patchsetId).commitId(patchset.commitId()).build();
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java
new file mode 100644
index 0000000..ea2acaa
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChange.java
@@ -0,0 +1,48 @@
+// 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 com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.Change;
+
+/** Representation of a change used for testing purposes. */
+@AutoValue
+public abstract class TestChange {
+
+ /**
+ * The numeric change ID, sometimes also called change number or legacy change ID. Unique per
+ * host.
+ */
+ public abstract Change.Id numericChangeId();
+
+ /**
+ * The Change-Id as specified in the commit message. Consists of an {@code I} followed by a 40-hex
+ * string. Only unique per project-branch.
+ */
+ public abstract String changeId();
+
+ static Builder builder() {
+ return new AutoValue_TestChange.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder numericChangeId(Change.Id numericChangeId);
+
+ abstract Builder changeId(String changeId);
+
+ abstract TestChange build();
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
new file mode 100644
index 0000000..65db967
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
@@ -0,0 +1,89 @@
+// 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 com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Constants;
+
+/** Initial attributes of the change. If not provided, arbitrary values will be used. */
+@AutoValue
+public abstract class TestChangeCreation {
+ public abstract Optional<Project.NameKey> project();
+
+ public abstract String branch();
+
+ public abstract Optional<Account.Id> owner();
+
+ public abstract String commitMessage();
+
+ public abstract ImmutableList<TreeModification> treeModifications();
+
+ abstract ThrowingFunction<TestChangeCreation, Change.Id> changeCreator();
+
+ public static Builder builder(ThrowingFunction<TestChangeCreation, Change.Id> changeCreator) {
+ return new AutoValue_TestChangeCreation.Builder()
+ .changeCreator(changeCreator)
+ .branch(Constants.R_HEADS + Constants.MASTER)
+ .commitMessage("A test change");
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ /** Target project/Repository of the change. Must be an existing project. */
+ public abstract Builder project(Project.NameKey project);
+
+ /**
+ * Target branch of the change. Neither needs to exist nor needs to point to an actual commit.
+ */
+ public abstract Builder branch(String branch);
+
+ /** The change owner. Must be an existing user account. */
+ public abstract Builder owner(Account.Id owner);
+
+ /**
+ * The commit message. The message may contain a {@code Change-Id} footer but does not need to.
+ * If the footer is absent, it will be generated.
+ */
+ public abstract Builder commitMessage(String commitMessage);
+
+ /** Modified file of the change. The file content is specified via the returned builder. */
+ public FileContentBuilder<Builder> file(String filePath) {
+ return new FileContentBuilder<>(this, filePath, treeModificationsBuilder()::add);
+ }
+
+ abstract ImmutableList.Builder<TreeModification> treeModificationsBuilder();
+
+ abstract Builder changeCreator(ThrowingFunction<TestChangeCreation, Change.Id> changeCreator);
+
+ abstract TestChangeCreation autoBuild();
+
+ /**
+ * Creates the change.
+ *
+ * @return the {@code Change.Id} of the created change
+ */
+ public Change.Id create() {
+ TestChangeCreation changeUpdate = autoBuild();
+ return changeUpdate.changeCreator().applyAndThrowSilently(changeUpdate);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java
new file mode 100644
index 0000000..1ba242a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchset.java
@@ -0,0 +1,43 @@
+// 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 com.google.auto.value.AutoValue;
+import com.google.gerrit.entities.PatchSet;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Representation of a patchset used for testing purposes. */
+@AutoValue
+public abstract class TestPatchset {
+
+ /** The numeric patchset ID. */
+ public abstract PatchSet.Id patchsetId();
+
+ /** The commit SHA-1 of the patchset. */
+ public abstract ObjectId commitId();
+
+ static Builder builder() {
+ return new AutoValue_TestPatchset.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder patchsetId(PatchSet.Id patchsetId);
+
+ abstract Builder commitId(ObjectId commitId);
+
+ abstract TestPatchset build();
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
new file mode 100644
index 0000000..571b1e1
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestPatchsetCreation.java
@@ -0,0 +1,61 @@
+// 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 com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.server.edit.tree.TreeModification;
+
+/** Initial attributes of the patchset. If not provided, arbitrary values will be used. */
+@AutoValue
+public abstract class TestPatchsetCreation {
+
+ public abstract ImmutableList<TreeModification> treeModifications();
+
+ abstract ThrowingFunction<TestPatchsetCreation, PatchSet.Id> patchsetCreator();
+
+ public static TestPatchsetCreation.Builder builder(
+ ThrowingFunction<TestPatchsetCreation, PatchSet.Id> patchsetCreator) {
+ return new AutoValue_TestPatchsetCreation.Builder().patchsetCreator(patchsetCreator);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ /** Modified file of the patchset. The file content is specified via the returned builder. */
+ public FileContentBuilder<Builder> file(String filePath) {
+ return new FileContentBuilder<>(this, filePath, treeModificationsBuilder()::add);
+ }
+
+ abstract ImmutableList.Builder<TreeModification> treeModificationsBuilder();
+
+ abstract TestPatchsetCreation.Builder patchsetCreator(
+ ThrowingFunction<TestPatchsetCreation, PatchSet.Id> patchsetCreator);
+
+ abstract TestPatchsetCreation autoBuild();
+
+ /**
+ * Creates the patchset.
+ *
+ * @return the {@code PatchSet.Id} of the created patchset
+ */
+ public PatchSet.Id create() {
+ TestPatchsetCreation patchsetCreation = autoBuild();
+ return patchsetCreation.patchsetCreator().applyAndThrowSilently(patchsetCreation);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
index d6fcb37..d344e18 100644
--- a/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/CommitInfoSubject.java
@@ -18,11 +18,13 @@
import static com.google.gerrit.extensions.common.testing.GitPersonSubject.gitPersons;
import static com.google.gerrit.truth.ListSubject.elements;
+import com.google.common.truth.Correspondence;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.truth.ListSubject;
+import com.google.gerrit.truth.NullAwareCorrespondence;
public class CommitInfoSubject extends Subject {
@@ -65,4 +67,8 @@
isNotNull();
return check("message").that(commitInfo.message);
}
+
+ public static Correspondence<CommitInfo, String> hasCommit() {
+ return NullAwareCorrespondence.transforming(commitInfo -> commitInfo.commit, "hasCommit");
+ }
}
diff --git a/java/com/google/gerrit/server/config/PluginConfig.java b/java/com/google/gerrit/server/config/PluginConfig.java
index 7dd981c..2459d0b 100644
--- a/java/com/google/gerrit/server/config/PluginConfig.java
+++ b/java/com/google/gerrit/server/config/PluginConfig.java
@@ -61,6 +61,13 @@
pluginName, copyConfig(cfg), Optional.ofNullable(projectConfig), groupReferences.build());
}
+ public static PluginConfig createFromGerritConfig(String pluginName, Config cfg) {
+ // There is no need to make a defensive copy here because this value won't be cached.
+ // gerrit.config uses baseConfig's (a member of Config) which would also make defensive copies
+ // fail.
+ return new AutoValue_PluginConfig(pluginName, cfg, Optional.empty(), ImmutableMap.of());
+ }
+
PluginConfig withInheritance(ProjectState.Factory projectStateFactory) {
checkState(projectConfig().isPresent(), "no project config provided");
diff --git a/java/com/google/gerrit/server/config/PluginConfigFactory.java b/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 0028d63..a9abd1e 100644
--- a/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -111,7 +111,7 @@
cfgSnapshot = FileSnapshot.save(configFile);
cfg = cfgProvider.get();
}
- return PluginConfig.create(pluginName, cfg, null);
+ return PluginConfig.createFromGerritConfig(pluginName, cfg);
}
/**
diff --git a/java/com/google/gerrit/truth/MapSubject.java b/java/com/google/gerrit/truth/MapSubject.java
index 95a0e0c..4eba753 100644
--- a/java/com/google/gerrit/truth/MapSubject.java
+++ b/java/com/google/gerrit/truth/MapSubject.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
import com.google.common.truth.IterableSubject;
import com.google.common.truth.Subject;
import java.util.Map;
@@ -51,4 +52,9 @@
isNotNull();
return check("values()").that(map.values());
}
+
+ public IntegerSubject size() {
+ isNotNull();
+ return check("size()").that(map.size());
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
new file mode 100644
index 0000000..4783db7
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImplTest.java
@@ -0,0 +1,462 @@
+// 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.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
+import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.hasCommit;
+import static com.google.gerrit.extensions.restapi.testing.BinaryResultSubject.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.MapSubject.assertThatMap;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.FileInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.inject.Inject;
+import java.util.Map;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class ChangeOperationsImplTest extends AbstractDaemonTest {
+
+ @Inject private ChangeOperations changeOperations;
+ @Inject private ProjectOperations projectOperations;
+ @Inject private AccountOperations accountOperations;
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ @Test
+ public void changeCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
+ Change.Id numericChangeId = changeOperations.newChange().create();
+
+ ChangeInfo change = getChangeFromServer(numericChangeId);
+ assertThat(change._number).isEqualTo(numericChangeId.get());
+ assertThat(change.changeId).isNotEmpty();
+ }
+
+ @Test
+ public void changeCanBeCreatedEvenWithRequestScopeOfArbitraryUser() throws Exception {
+ Account.Id user = accountOperations.newAccount().create();
+
+ requestScopeOperations.setApiUser(user);
+ Change.Id numericChangeId = changeOperations.newChange().create();
+
+ ChangeInfo change = getChangeFromServer(numericChangeId);
+ assertThat(change._number).isEqualTo(numericChangeId.get());
+ }
+
+ @Test
+ public void twoChangesWithoutAnyParametersDoNotClash() {
+ Change.Id changeId1 = changeOperations.newChange().create();
+ Change.Id changeId2 = changeOperations.newChange().create();
+
+ TestChange change1 = changeOperations.change(changeId1).get();
+ TestChange change2 = changeOperations.change(changeId2).get();
+ assertThat(change1.numericChangeId()).isNotEqualTo(change2.numericChangeId());
+ assertThat(change1.changeId()).isNotEqualTo(change2.changeId());
+ }
+
+ @Test
+ public void twoSubsequentlyCreatedChangesDoNotDependOnEachOther() throws Exception {
+ Change.Id changeId1 = changeOperations.newChange().create();
+ Change.Id changeId2 = changeOperations.newChange().create();
+
+ ChangeInfo change1 = getChangeFromServer(changeId1);
+ ChangeInfo change2 = getChangeFromServer(changeId2);
+ CommitInfo currentPatchsetCommit1 = change1.revisions.get(change1.currentRevision).commit;
+ CommitInfo currentPatchsetCommit2 = change2.revisions.get(change2.currentRevision).commit;
+ assertThat(currentPatchsetCommit1)
+ .parents()
+ .comparingElementsUsing(hasCommit())
+ .doesNotContain(currentPatchsetCommit2.commit);
+ assertThat(currentPatchsetCommit2)
+ .parents()
+ .comparingElementsUsing(hasCommit())
+ .doesNotContain(currentPatchsetCommit1.commit);
+ }
+
+ @Test
+ public void createdChangeHasAtLeastOnePatchset() throws Exception {
+ Change.Id changeId = changeOperations.newChange().create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ assertThatMap(change.revisions).size().isAtLeast(1);
+ }
+
+ @Test
+ public void createdChangeIsInSpecifiedProject() throws Exception {
+ Project.NameKey project = projectOperations.newProject().create();
+ Change.Id changeId = changeOperations.newChange().project(project).create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ assertThat(change.project).isEqualTo(project.get());
+ }
+
+ @Test
+ public void changeCanBeCreatedInEmptyRepository() throws Exception {
+ Project.NameKey project = projectOperations.newProject().noEmptyCommit().create();
+ Change.Id changeId = changeOperations.newChange().project(project).create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ assertThat(change.project).isEqualTo(project.get());
+ }
+
+ @Test
+ public void createdChangeHasSpecifiedTargetBranch() throws Exception {
+ Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
+ Change.Id changeId =
+ changeOperations.newChange().project(project).branch("test-branch").create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ assertThat(change.branch).isEqualTo("test-branch");
+ }
+
+ @Test
+ public void createdChangeUsesTipOfTargetBranchAsParentByDefault() throws Exception {
+ Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
+ ObjectId parentCommitId = projectOperations.project(project).getHead("test-branch").getId();
+ Change.Id changeId =
+ changeOperations.newChange().project(project).branch("test-branch").create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
+ assertThat(currentPatchsetCommit)
+ .parents()
+ .onlyElement()
+ .commit()
+ .isEqualTo(parentCommitId.getName());
+ }
+
+ @Test
+ public void createdChangeHasSpecifiedOwner() throws Exception {
+ Account.Id changeOwner = accountOperations.newAccount().create();
+ Change.Id changeId = changeOperations.newChange().owner(changeOwner).create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ assertThat(change.owner._accountId).isEqualTo(changeOwner.get());
+ }
+
+ @Test
+ public void changeOwnerDoesNotNeedAnyPermissionsForChangeCreation() throws Exception {
+ Account.Id changeOwner = accountOperations.newAccount().create();
+ Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
+ // Remove any read and push permissions which might potentially exist. Without read, users
+ // shouldn't be able to do anything. The newly created project should only inherit from
+ // All-Projects.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .remove(permissionKey(Permission.READ).ref("refs/heads/test-branch"))
+ .remove(permissionKey(Permission.PUSH).ref("refs/heads/test-branch"))
+ .update();
+ projectOperations
+ .allProjectsForUpdate()
+ .remove(permissionKey(Permission.READ).ref("refs/heads/test-branch"))
+ .remove(permissionKey(Permission.PUSH).ref("refs/heads/test-branch"))
+ .update();
+
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .owner(changeOwner)
+ .branch("test-branch")
+ .project(project)
+ .create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ assertThat(change.owner._accountId).isEqualTo(changeOwner.get());
+ }
+
+ @Test
+ public void createdChangeHasSpecifiedCommitMessage() throws Exception {
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .commitMessage("Summary line\n\nDetailed description.")
+ .create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
+ assertThat(currentPatchsetCommit).message().startsWith("Summary line\n\nDetailed description.");
+ }
+
+ @Test
+ public void changeCannotBeCreatedWithoutCommitMessage() {
+ assertThrows(
+ IllegalStateException.class, () -> changeOperations.newChange().commitMessage("").create());
+ }
+
+ @Test
+ public void commitMessageOfCreatedChangeAutomaticallyGetsChangeId() throws Exception {
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .commitMessage("Summary line\n\nDetailed description.")
+ .create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
+ assertThat(currentPatchsetCommit).message().contains("Change-Id:");
+ }
+
+ @Test
+ public void changeIdSpecifiedInCommitMessageIsKeptForCreatedChange() throws Exception {
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .commitMessage("Summary line\n\nChange-Id: I0123456789012345678901234567890123456789")
+ .create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
+ assertThat(currentPatchsetCommit)
+ .message()
+ .contains("Change-Id: I0123456789012345678901234567890123456789");
+ assertThat(change.changeId).isEqualTo("I0123456789012345678901234567890123456789");
+ }
+
+ @Test
+ public void createdChangeHasSpecifiedFiles() throws Exception {
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .file("file1")
+ .content("Line 1")
+ .file("path/to/file2.txt")
+ .content("Line one")
+ .create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ Map<String, FileInfo> files = change.revisions.get(change.currentRevision).files;
+ assertThatMap(files).keys().containsExactly("file1", "path/to/file2.txt");
+ BinaryResult fileContent1 = gApi.changes().id(changeId.get()).current().file("file1").content();
+ assertThat(fileContent1).asString().isEqualTo("Line 1");
+ BinaryResult fileContent2 =
+ gApi.changes().id(changeId.get()).current().file("path/to/file2.txt").content();
+ assertThat(fileContent2).asString().isEqualTo("Line one");
+ }
+
+ @Test
+ public void existingChangeCanBeCheckedForExistence() {
+ Change.Id changeId = changeOperations.newChange().create();
+
+ boolean exists = changeOperations.change(changeId).exists();
+
+ assertThat(exists).isTrue();
+ }
+
+ @Test
+ public void notExistingChangeCanBeCheckedForExistence() {
+ Change.Id changeId = Change.id(123456789);
+
+ boolean exists = changeOperations.change(changeId).exists();
+
+ assertThat(exists).isFalse();
+ }
+
+ @Test
+ public void retrievingNotExistingChangeFails() {
+ Change.Id changeId = Change.id(123456789);
+ assertThrows(IllegalStateException.class, () -> changeOperations.change(changeId).get());
+ }
+
+ @Test
+ public void numericChangeIdOfExistingChangeCanBeRetrieved() {
+ Change.Id changeId = changeOperations.newChange().create();
+
+ TestChange change = changeOperations.change(changeId).get();
+ assertThat(change.numericChangeId()).isEqualTo(changeId);
+ }
+
+ @Test
+ public void changeIdOfExistingChangeCanBeRetrieved() {
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .commitMessage("Summary line\n\nChange-Id: I0123456789012345678901234567890123456789")
+ .create();
+
+ TestChange change = changeOperations.change(changeId).get();
+ assertThat(change.changeId()).isEqualTo("I0123456789012345678901234567890123456789");
+ }
+
+ @Test
+ public void currentPatchsetOfExistingChangeCanBeRetrieved() throws Exception {
+ Change.Id changeId = changeOperations.newChange().create();
+
+ TestPatchset patchset = changeOperations.change(changeId).currentPatchset().get();
+
+ ChangeInfo expectedChange = getChangeFromServer(changeId);
+ String expectedCommitId = expectedChange.currentRevision;
+ int expectedPatchsetNumber = expectedChange.revisions.get(expectedCommitId)._number;
+ assertThat(patchset.commitId()).isEqualTo(ObjectId.fromString(expectedCommitId));
+ assertThat(patchset.patchsetId()).isEqualTo(PatchSet.id(changeId, expectedPatchsetNumber));
+ }
+
+ @Test
+ public void earlierPatchsetOfExistingChangeCanBeRetrieved() {
+ Change.Id changeId = changeOperations.newChange().create();
+ PatchSet.Id earlierPatchsetId =
+ changeOperations.change(changeId).currentPatchset().get().patchsetId();
+ PatchSet.Id currentPatchsetId = changeOperations.change(changeId).newPatchset().create();
+
+ TestPatchset earlierPatchset =
+ changeOperations.change(changeId).patchset(earlierPatchsetId).get();
+
+ assertThat(earlierPatchset.patchsetId()).isEqualTo(earlierPatchsetId);
+ assertThat(earlierPatchset.patchsetId()).isNotEqualTo(currentPatchsetId);
+ }
+
+ @Test
+ public void newPatchsetCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
+ Change.Id changeId = changeOperations.newChange().create();
+ ChangeInfo unmodifiedChange = getChangeFromServer(changeId);
+ int originalPatchsetCount = unmodifiedChange.revisions.size();
+
+ PatchSet.Id patchsetId = changeOperations.change(changeId).newPatchset().create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ assertThatMap(change.revisions).hasSize(originalPatchsetCount + 1);
+ RevisionInfo currentRevision = change.revisions.get(change.currentRevision);
+ assertThat(currentRevision._number).isEqualTo(patchsetId.get());
+ }
+
+ @Test
+ public void newPatchsetIsCopyOfPreviousPatchsetByDefault() throws Exception {
+ Change.Id changeId = changeOperations.newChange().create();
+
+ PatchSet.Id patchsetId = changeOperations.change(changeId).newPatchset().create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ RevisionInfo patchsetRevision = getRevision(change, patchsetId);
+ assertThat(patchsetRevision.kind).isEqualTo(ChangeKind.NO_CHANGE);
+ }
+
+ @Test
+ public void newPatchsetCanHaveReplacedFileContent() throws Exception {
+ Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").create();
+
+ PatchSet.Id patchsetId =
+ changeOperations
+ .change(changeId)
+ .newPatchset()
+ .file("file1")
+ .content("Different content")
+ .create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ Map<String, FileInfo> files = change.revisions.get(change.currentRevision).files;
+ assertThatMap(files).keys().containsExactly("file1");
+ BinaryResult fileContent = getFileContent(changeId, patchsetId, "file1");
+ assertThat(fileContent).asString().isEqualTo("Different content");
+ }
+
+ @Test
+ public void newPatchsetCanHaveAdditionalFile() throws Exception {
+ Change.Id changeId = changeOperations.newChange().file("file1").content("Line 1").create();
+
+ PatchSet.Id patchsetId =
+ changeOperations
+ .change(changeId)
+ .newPatchset()
+ .file("file2")
+ .content("My file content")
+ .create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ Map<String, FileInfo> files = change.revisions.get(change.currentRevision).files;
+ assertThatMap(files).keys().containsExactly("file1", "file2");
+ BinaryResult fileContent = getFileContent(changeId, patchsetId, "file2");
+ assertThat(fileContent).asString().isEqualTo("My file content");
+ }
+
+ @Test
+ public void newPatchsetCanHaveLessFiles() throws Exception {
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .file("file1")
+ .content("Line 1")
+ .file("file2")
+ .content("Line one")
+ .create();
+
+ changeOperations.change(changeId).newPatchset().file("file2").delete().create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ Map<String, FileInfo> files = change.revisions.get(change.currentRevision).files;
+ assertThatMap(files).keys().containsExactly("file1");
+ }
+
+ @Test
+ public void newPatchsetCanHaveRenamedFile() throws Exception {
+ Change.Id changeId =
+ changeOperations
+ .newChange()
+ .file("file1")
+ .content("Line 1")
+ .file("file2")
+ .content("Line one")
+ .create();
+
+ PatchSet.Id patchsetId =
+ changeOperations
+ .change(changeId)
+ .newPatchset()
+ .file("file2")
+ .renameTo("renamed file")
+ .create();
+
+ ChangeInfo change = getChangeFromServer(changeId);
+ Map<String, FileInfo> files = change.revisions.get(change.currentRevision).files;
+ assertThatMap(files).keys().containsExactly("file1", "renamed file");
+ BinaryResult fileContent = getFileContent(changeId, patchsetId, "renamed file");
+ assertThat(fileContent).asString().isEqualTo("Line one");
+ }
+
+ private ChangeInfo getChangeFromServer(Change.Id changeId) throws RestApiException {
+ return gApi.changes().id(changeId.get()).get();
+ }
+
+ private RevisionInfo getRevision(ChangeInfo change, PatchSet.Id patchsetId) {
+ return change.revisions.values().stream()
+ .filter(revision -> revision._number == patchsetId.get())
+ .findAny()
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ String.format(
+ "Change %d doesn't have specified patchset %d.",
+ change._number, patchsetId.get())));
+ }
+
+ private BinaryResult getFileContent(Change.Id changeId, PatchSet.Id patchsetId, String filePath)
+ throws RestApiException {
+ return gApi.changes().id(changeId.get()).revision(patchsetId.get()).file(filePath).content();
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
deleted file mode 100644
index 2292ae7..0000000
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 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.
- */
-import '../gr-button/gr-button.js';
-import '../../../styles/shared-styles.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-dialog_html.js';
-
-/**
- * @extends PolymerElement
- */
-class GrDialog extends GestureEventListeners(
- LegacyElementMixin(PolymerElement)) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-dialog'; }
- /**
- * Fired when the confirm button is pressed.
- *
- * @event confirm
- */
-
- /**
- * Fired when the cancel button is pressed.
- *
- * @event cancel
- */
-
- static get properties() {
- return {
- confirmLabel: {
- type: String,
- value: 'Confirm',
- },
- // Supplying an empty cancel label will hide the button completely.
- cancelLabel: {
- type: String,
- value: 'Cancel',
- },
- disabled: {
- type: Boolean,
- value: false,
- },
- confirmOnEnter: {
- type: Boolean,
- value: false,
- },
- confirmTooltip: {
- type: String,
- observer: '_handleConfirmTooltipUpdate',
- },
- };
- }
-
- /** @override */
- ready() {
- super.ready();
- this._ensureAttribute('role', 'dialog');
- }
-
- _handleConfirmTooltipUpdate(confirmTooltip) {
- if (confirmTooltip) {
- this.$.confirm.setAttribute('has-tooltip', true);
- } else {
- this.$.confirm.removeAttribute('has-tooltip');
- }
- }
-
- _handleConfirm(e) {
- if (this.disabled) { return; }
-
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('confirm', {
- composed: true, bubbles: false,
- }));
- }
-
- _handleCancelTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this.dispatchEvent(new CustomEvent('cancel', {
- composed: true, bubbles: false,
- }));
- }
-
- _handleKeydown(e) {
- if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); }
- }
-
- resetFocus() {
- this.$.confirm.focus();
- }
-
- _computeCancelClass(cancelLabel) {
- return cancelLabel.length ? '' : 'hidden';
- }
-}
-
-customElements.define(GrDialog.is, GrDialog);
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
new file mode 100644
index 0000000..fa6403a
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.ts
@@ -0,0 +1,128 @@
+/**
+ * @license
+ * Copyright (C) 2016 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.
+ */
+import '../gr-button/gr-button';
+import '../../../styles/shared-styles';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {htmlTemplate} from './gr-dialog_html';
+import {customElement, property, observe} from '@polymer/decorators';
+import {GrButton} from '../gr-button/gr-button';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-dialog': GrDialog;
+ }
+}
+
+export interface GrDialog {
+ $: {
+ confirm: GrButton;
+ };
+}
+
+@customElement('gr-dialog')
+export class GrDialog extends GestureEventListeners(
+ LegacyElementMixin(PolymerElement)
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when the confirm button is pressed.
+ *
+ * @event confirm
+ */
+
+ /**
+ * Fired when the cancel button is pressed.
+ *
+ * @event cancel
+ */
+
+ @property({type: String})
+ confirmLabel = 'Confirm';
+
+ // Supplying an empty cancel label will hide the button completely.
+ @property({type: String})
+ cancelLabel = 'Cancel';
+
+ @property({type: Boolean})
+ disabled = false;
+
+ @property({type: Boolean})
+ confirmOnEnter = false;
+
+ @property({type: String})
+ confirmTooltip?: string;
+
+ /** @override */
+ ready() {
+ super.ready();
+ this._ensureAttribute('role', 'dialog');
+ }
+
+ @observe('confirmTooltip')
+ _handleConfirmTooltipUpdate(confirmTooltip?: string) {
+ if (confirmTooltip) {
+ this.$.confirm.setAttribute('has-tooltip', 'true');
+ } else {
+ this.$.confirm.removeAttribute('has-tooltip');
+ }
+ }
+
+ _handleConfirm(e: KeyboardEvent) {
+ if (this.disabled) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(
+ new CustomEvent('confirm', {
+ composed: true,
+ bubbles: false,
+ })
+ );
+ }
+
+ _handleCancelTap(e: MouseEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(
+ new CustomEvent('cancel', {
+ composed: true,
+ bubbles: false,
+ })
+ );
+ }
+
+ _handleKeydown(e: KeyboardEvent) {
+ if (this.confirmOnEnter && e.keyCode === 13) {
+ this._handleConfirm(e);
+ }
+ }
+
+ resetFocus() {
+ this.$.confirm.focus();
+ }
+
+ _computeCancelClass(cancelLabel: string) {
+ return cancelLabel.length ? '' : 'hidden';
+ }
+}