Merge changes from topic "gr-dialog-to-ts"
* changes:
Move gr-dialog 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/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index c894059..e4d594b 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance.testsuite.project;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -43,12 +44,10 @@
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collections;
import org.apache.commons.lang.RandomStringUtils;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -91,7 +90,8 @@
CreateProjectArgs args = new CreateProjectArgs();
args.setProjectName(name);
- args.branch = Collections.singletonList(Constants.R_HEADS + Constants.MASTER);
+ args.branch =
+ projectCreation.branches().stream().map(RefNames::fullName).collect(toImmutableList());
args.createEmptyCommit = projectCreation.createEmptyCommit().orElse(true);
projectCreation.parent().ifPresent(p -> args.newParent = p);
// ProjectCreator wants non-null owner IDs.
@@ -211,9 +211,7 @@
}
private RevCommit headOrNull(String branch) {
- if (!branch.startsWith(Constants.R_REFS)) {
- branch = RefNames.REFS_HEADS + branch;
- }
+ branch = RefNames.fullName(branch);
try (Repository repo = repoManager.openRepository(nameKey);
RevWalk rw = new RevWalk(repo)) {
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
index 99e045c..2649dea 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectCreation.java
@@ -15,10 +15,14 @@
package com.google.gerrit.acceptance.testsuite.project;
import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.SubmitType;
import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jgit.lib.Constants;
@AutoValue
public abstract class TestProjectCreation {
@@ -27,6 +31,8 @@
public abstract Optional<Project.NameKey> parent();
+ public abstract ImmutableSet<String> branches();
+
public abstract Optional<Boolean> createEmptyCommit();
public abstract Optional<SubmitType> submitType();
@@ -35,7 +41,9 @@
public static Builder builder(
ThrowingFunction<TestProjectCreation, Project.NameKey> projectCreator) {
- return new AutoValue_TestProjectCreation.Builder().projectCreator(projectCreator);
+ return new AutoValue_TestProjectCreation.Builder()
+ .branches(Constants.R_HEADS + Constants.MASTER)
+ .projectCreator(projectCreator);
}
@AutoValue.Builder
@@ -46,6 +54,17 @@
public abstract TestProjectCreation.Builder submitType(SubmitType submitType);
+ /**
+ * Branches which should be created in the repository (with an empty root commit). The
+ * "refs/heads/" prefix of the branch name can be omitted. The specified branches are ignored if
+ * {@link #noEmptyCommit()} is used.
+ */
+ public TestProjectCreation.Builder branches(String branch1, String... otherBranches) {
+ return branches(Sets.union(ImmutableSet.of(branch1), ImmutableSet.copyOf(otherBranches)));
+ }
+
+ abstract TestProjectCreation.Builder branches(Set<String> branches);
+
public abstract TestProjectCreation.Builder createEmptyCommit(boolean value);
/** Skips the empty commit on creation. This means that project's branches will not exist. */
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/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 48683ea..a05c392 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -498,7 +498,7 @@
throws BadRequestException, IOException, InvalidChangeOperationException {
ObjectId newTreeId;
try {
- TreeCreator treeCreator = new TreeCreator(baseCommit);
+ TreeCreator treeCreator = TreeCreator.basedOn(baseCommit);
treeCreator.addTreeModifications(treeModifications);
newTreeId = treeCreator.createNewTreeAndGetId(repository);
} catch (InvalidPathException e) {
diff --git a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index 0adacd8..4821a7a 100644
--- a/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -18,6 +18,7 @@
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import com.google.gerrit.extensions.restapi.RawInput;
@@ -33,7 +34,6 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
/** A {@code TreeModification} which changes the content of a file. */
public class ChangeFileContentModification implements TreeModification {
@@ -48,7 +48,8 @@
}
@Override
- public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, RevCommit baseCommit) {
+ public List<DirCacheEditor.PathEdit> getPathEdits(
+ Repository repository, ObjectId treeId, ImmutableList<? extends ObjectId> parents) {
DirCacheEditor.PathEdit changeContentEdit = new ChangeContent(filePath, newContent, repository);
return Collections.singletonList(changeContentEdit);
}
diff --git a/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java b/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
index feffb70..3b7826c 100644
--- a/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
+++ b/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
@@ -14,11 +14,12 @@
package com.google.gerrit.server.edit.tree;
+import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
/** A {@code TreeModification} which deletes a file. */
public class DeleteFileModification implements TreeModification {
@@ -30,7 +31,8 @@
}
@Override
- public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, RevCommit baseCommit) {
+ public List<DirCacheEditor.PathEdit> getPathEdits(
+ Repository repository, ObjectId treeId, ImmutableList<? extends ObjectId> parents) {
DirCacheEditor.DeletePath deletePathEdit = new DirCacheEditor.DeletePath(filePath);
return Collections.singletonList(deletePathEdit);
}
diff --git a/java/com/google/gerrit/server/edit/tree/RenameFileModification.java b/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
index b847599..d4412e4 100644
--- a/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
+++ b/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
@@ -14,13 +14,12 @@
package com.google.gerrit.server.edit.tree;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
@@ -36,19 +35,23 @@
}
@Override
- public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, RevCommit baseCommit)
+ public List<DirCacheEditor.PathEdit> getPathEdits(
+ Repository repository, ObjectId treeId, ImmutableList<? extends ObjectId> parents)
throws IOException {
+ if (ObjectId.zeroId().equals(treeId)) {
+ return ImmutableList.of();
+ }
+
try (RevWalk revWalk = new RevWalk(repository)) {
- revWalk.parseHeaders(baseCommit);
try (TreeWalk treeWalk =
- TreeWalk.forPath(revWalk.getObjectReader(), currentFilePath, baseCommit.getTree())) {
+ TreeWalk.forPath(revWalk.getObjectReader(), currentFilePath, treeId)) {
if (treeWalk == null) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
DirCacheEditor.DeletePath deletePathEdit = new DirCacheEditor.DeletePath(currentFilePath);
AddPath addPathEdit =
new AddPath(newFilePath, treeWalk.getFileMode(0), treeWalk.getObjectId(0));
- return Arrays.asList(deletePathEdit, addPathEdit);
+ return ImmutableList.of(deletePathEdit, addPathEdit);
}
}
}
diff --git a/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java b/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
index 393a866..730664b 100644
--- a/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
+++ b/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
@@ -14,10 +14,12 @@
package com.google.gerrit.server.edit.tree;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -36,16 +38,16 @@
}
@Override
- public List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, RevCommit baseCommit)
+ public List<DirCacheEditor.PathEdit> getPathEdits(
+ Repository repository, ObjectId treeId, ImmutableList<? extends ObjectId> parents)
throws IOException {
- if (baseCommit.getParentCount() == 0) {
+ if (parents.isEmpty()) {
DirCacheEditor.DeletePath deletePath = new DirCacheEditor.DeletePath(filePath);
return Collections.singletonList(deletePath);
}
- RevCommit base = baseCommit.getParent(0);
try (RevWalk revWalk = new RevWalk(repository)) {
- revWalk.parseHeaders(base);
+ RevCommit base = revWalk.parseCommit(parents.get(0));
try (TreeWalk treeWalk =
TreeWalk.forPath(revWalk.getObjectReader(), filePath, base.getTree())) {
if (treeWalk == null) {
diff --git a/java/com/google/gerrit/server/edit/tree/TreeCreator.java b/java/com/google/gerrit/server/edit/tree/TreeCreator.java
index e6caf97..3c400ec 100644
--- a/java/com/google/gerrit/server/edit/tree/TreeCreator.java
+++ b/java/com/google/gerrit/server/edit/tree/TreeCreator.java
@@ -16,6 +16,7 @@
import static java.util.Objects.requireNonNull;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -31,15 +32,26 @@
/**
* A creator for a new Git tree. To create the new tree, the tree of another commit is taken as a
- * basis and modified.
+ * basis and modified. Alternatively, an empty tree can serve as base.
*/
public class TreeCreator {
- private final RevCommit baseCommit;
+ private final ObjectId baseTreeId;
+ private final ImmutableList<? extends ObjectId> baseParents;
private final List<TreeModification> treeModifications = new ArrayList<>();
- public TreeCreator(RevCommit baseCommit) {
- this.baseCommit = requireNonNull(baseCommit, "baseCommit is required");
+ public static TreeCreator basedOn(RevCommit baseCommit) {
+ requireNonNull(baseCommit, "baseCommit is required");
+ return new TreeCreator(baseCommit.getTree(), ImmutableList.copyOf(baseCommit.getParents()));
+ }
+
+ public static TreeCreator basedOnEmptyTree() {
+ return new TreeCreator(ObjectId.zeroId(), ImmutableList.of());
+ }
+
+ private TreeCreator(ObjectId baseTreeId, ImmutableList<? extends ObjectId> baseParents) {
+ this.baseTreeId = requireNonNull(baseTreeId, "baseTree is required");
+ this.baseParents = baseParents;
}
/**
@@ -78,8 +90,9 @@
try (ObjectReader objectReader = repository.newObjectReader()) {
DirCache dirCache = DirCache.newInCore();
DirCacheBuilder dirCacheBuilder = dirCache.builder();
- dirCacheBuilder.addTree(
- new byte[0], DirCacheEntry.STAGE_0, objectReader, baseCommit.getTree());
+ if (!ObjectId.zeroId().equals(baseTreeId)) {
+ dirCacheBuilder.addTree(new byte[0], DirCacheEntry.STAGE_0, objectReader, baseTreeId);
+ }
dirCacheBuilder.finish();
return dirCache;
}
@@ -88,7 +101,8 @@
private List<DirCacheEditor.PathEdit> getPathEdits(Repository repository) throws IOException {
List<DirCacheEditor.PathEdit> pathEdits = new ArrayList<>();
for (TreeModification treeModification : treeModifications) {
- pathEdits.addAll(treeModification.getPathEdits(repository, baseCommit));
+ pathEdits.addAll(
+ treeModification.getPathEdits(repository, baseTreeId, ImmutableList.copyOf(baseParents)));
}
return pathEdits;
}
diff --git a/java/com/google/gerrit/server/edit/tree/TreeModification.java b/java/com/google/gerrit/server/edit/tree/TreeModification.java
index 2656707..68fd7ea 100644
--- a/java/com/google/gerrit/server/edit/tree/TreeModification.java
+++ b/java/com/google/gerrit/server/edit/tree/TreeModification.java
@@ -15,11 +15,12 @@
package com.google.gerrit.server.edit.tree;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
/** A specific modification of a Git tree. */
public interface TreeModification {
@@ -30,11 +31,14 @@
* shouldn't be changed.
*
* @param repository the affected Git repository
- * @param baseCommit the commit to whose tree this modification is applied
+ * @param treeId tree to which the modification is applied. A value of {@code ObjectId.zero()}
+ * indicates an empty tree.
+ * @param parents parent commits of the commit to whose tree this modification is applied
* @return an ordered list of necessary {@code PathEdit}s
* @throws IOException if problems arise when accessing the repository
*/
- List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, RevCommit baseCommit)
+ List<DirCacheEditor.PathEdit> getPathEdits(
+ Repository repository, ObjectId treeId, ImmutableList<? extends ObjectId> parents)
throws IOException;
/**
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 663c9aa..d2e1086 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -56,7 +56,6 @@
import com.google.inject.name.Named;
import java.io.IOException;
import java.time.Duration;
-import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@@ -224,9 +223,7 @@
@Override
public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
- return all().stream()
- .map(n -> byName.getIfPresent(n))
- .filter(Objects::nonNull)
+ return byName.asMap().values().stream()
.flatMap(p -> p.getConfig().getAllGroupUUIDs().stream())
// getAllGroupUUIDs shouldn't really return null UUIDs, but harden
// against them just in case there is a bug or corner case.
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/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index 76b8826..62dfc63 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -36,6 +36,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.truth.Correspondence;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.TestPermission;
import com.google.gerrit.entities.Permission;
@@ -43,6 +44,7 @@
import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.truth.NullAwareCorrespondence;
import com.google.inject.Inject;
import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
@@ -82,6 +84,36 @@
}
@Test
+ public void specifiedBranchesAreCreatedInNewProject() throws Exception {
+ Project.NameKey project =
+ projectOperations
+ .newProject()
+ .branches("test-branch", "refs/heads/another-test-branch")
+ .create();
+
+ List<BranchInfo> branches = gApi.projects().name(project.get()).branches().get();
+ assertThat(branches)
+ .comparingElementsUsing(hasBranchName())
+ .containsAtLeast("refs/heads/test-branch", "refs/heads/another-test-branch");
+ }
+
+ @Test
+ public void specifiedBranchesAreNotCreatedInNewProjectIfNoEmptyCommitRequested()
+ throws Exception {
+ Project.NameKey project =
+ projectOperations
+ .newProject()
+ .branches("test-branch", "refs/heads/another-test-branch")
+ .noEmptyCommit()
+ .create();
+
+ List<BranchInfo> branches = gApi.projects().name(project.get()).branches().get();
+ assertThat(branches)
+ .comparingElementsUsing(hasBranchName())
+ .containsNoneOf("refs/heads/test-branch", "refs/heads/another-test-branch");
+ }
+
+ @Test
public void getProjectConfig() throws Exception {
Project.NameKey key = projectOperations.newProject().create();
assertThat(projectOperations.project(key).getProjectConfig().getProject().getDescription())
@@ -571,4 +603,8 @@
tr.delete(REFS_CONFIG);
}
}
+
+ private static Correspondence<BranchInfo, String> hasBranchName() {
+ return NullAwareCorrespondence.transforming(branch -> branch.ref, "hasName");
+ }
}
diff --git a/javatests/com/google/gerrit/server/edit/tree/TreeCreatorTest.java b/javatests/com/google/gerrit/server/edit/tree/TreeCreatorTest.java
new file mode 100644
index 0000000..926c345
--- /dev/null
+++ b/javatests/com/google/gerrit/server/edit/tree/TreeCreatorTest.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit.tree;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.RawInputUtil;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TreeCreatorTest {
+
+ private Repository repository;
+ private TestRepository<?> testRepository;
+
+ @Before
+ public void setUp() throws Exception {
+ repository = new InMemoryRepository(new DfsRepositoryDescription("Test Repository"));
+ testRepository = new TestRepository<>(repository);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (testRepository != null) {
+ testRepository.close();
+ }
+ }
+
+ @Test
+ public void fileContentModificationWorksWithEmptyTree() throws Exception {
+ TreeCreator treeCreator = TreeCreator.basedOnEmptyTree();
+ treeCreator.addTreeModifications(
+ ImmutableList.of(
+ new ChangeFileContentModification("file.txt", RawInputUtil.create("Line 1"))));
+ ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+
+ String fileContent = getFileContent(newTreeId, "file.txt");
+ assertThat(fileContent).isEqualTo("Line 1");
+ }
+
+ @Test
+ public void renameFileModificationDoesNotComplainAboutEmptyTree() throws Exception {
+ TreeCreator treeCreator = TreeCreator.basedOnEmptyTree();
+ treeCreator.addTreeModifications(
+ ImmutableList.of(new RenameFileModification("oldfileName", "newFileName")));
+ ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+
+ assertThat(isEmptyTree(newTreeId)).isTrue();
+ }
+
+ @Test
+ public void deleteFileModificationDoesNotComplainAboutEmptyTree() throws Exception {
+ TreeCreator treeCreator = TreeCreator.basedOnEmptyTree();
+ treeCreator.addTreeModifications(ImmutableList.of(new DeleteFileModification("file.txt")));
+ ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+
+ assertThat(isEmptyTree(newTreeId)).isTrue();
+ }
+
+ @Test
+ public void restoreFileModificationDoesNotComplainAboutEmptyTree() throws Exception {
+ TreeCreator treeCreator = TreeCreator.basedOnEmptyTree();
+ treeCreator.addTreeModifications(ImmutableList.of(new RestoreFileModification("file.txt")));
+ ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
+
+ assertThat(isEmptyTree(newTreeId)).isTrue();
+ }
+
+ private String getFileContent(ObjectId treeId, String filePath) throws Exception {
+ try (RevWalk revWalk = new RevWalk(repository);
+ ObjectReader reader = revWalk.getObjectReader()) {
+ RevTree revTree = revWalk.parseTree(treeId);
+ RevObject revObject = testRepository.get(revTree, filePath);
+ return new String(reader.open(revObject, OBJ_BLOB).getBytes(), UTF_8);
+ }
+ }
+
+ private boolean isEmptyTree(ObjectId treeId) throws Exception {
+ try (TreeWalk treeWalk = new TreeWalk(repository)) {
+ treeWalk.reset(treeId);
+ return !treeWalk.next();
+ }
+ }
+}
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index bf1fcc6..6d19ac9 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -268,6 +268,8 @@
"rules": {
// The following rules is required to match internal google rules
"@typescript-eslint/restrict-plus-operands": "error",
+ // https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/node-builtins.md
+ "node/no-unsupported-features/node-builtins": "off",
},
"parserOptions": {
"project": path.resolve(__dirname, "./tsconfig_eslint.json"),
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
deleted file mode 100644
index 1e37603..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-class CustomPluginHeader extends PolymerElement {
- static get is() {
- return 'gr-custom-plugin-header';
- }
-
- static get properties() {
- return {
- logoUrl: String,
- title: String,
- };
- }
-
- static get template() {
- return html`
- <style>
- img {
- width: 1em;
- height: 1em;
- vertical-align: middle;
- }
- .title {
- margin-left: var(--spacing-xs);
- }
- </style>
- <span>
- <img src="[[logoUrl]]" hidden\$="[[!logoUrl]]">
- <span class="title">[[title]]</span>
- </span>
-`;
- }
-}
-
-customElements.define(CustomPluginHeader.is, CustomPluginHeader);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
new file mode 100644
index 0000000..7d82ff4
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.ts
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright (C) 2017 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 {PolymerElement} from '@polymer/polymer/polymer-element';
+import {html} from '@polymer/polymer/lib/utils/html-tag';
+import {customElement, property} from '@polymer/decorators';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-custom-plugin-header': GrCustomPluginHeader;
+ }
+}
+
+@customElement('gr-custom-plugin-header')
+export class GrCustomPluginHeader extends PolymerElement {
+ @property({type: String})
+ logoUrl = '';
+
+ @property({type: String})
+ title = '';
+
+ static get template() {
+ return html`
+ <style>
+ img {
+ width: 1em;
+ height: 1em;
+ vertical-align: middle;
+ }
+ .title {
+ margin-left: var(--spacing-xs);
+ }
+ </style>
+ <span>
+ <img src="[[logoUrl]]" hidden$="[[!logoUrl]]" />
+ <span class="title">[[title]]</span>
+ </span>
+ `;
+ }
+}
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
deleted file mode 100644
index 48b14d3..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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-custom-plugin-header.js';
-
-/** @constructor */
-export function GrThemeApi(plugin) {
- this.plugin = plugin;
-}
-
-GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) {
- this.plugin.hook('header-title', {replace: true}).onAttached(
- element => {
- const customHeader =
- document.createElement('gr-custom-plugin-header');
- customHeader.logoUrl = logoUrl;
- customHeader.title = title;
- element.appendChild(customHeader);
- });
-};
-
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts
new file mode 100644
index 0000000..613bde2
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright (C) 2017 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-custom-plugin-header';
+import {GrCustomPluginHeader} from './gr-custom-plugin-header';
+
+// TODO(TS): replace with Plugin once gr-public-js-api migrated
+interface PluginApi {
+ hook(
+ endpointName: string,
+ option: {replace?: boolean}
+ ): {
+ onAttached(callback: (el: Element) => void): void;
+ };
+}
+
+/**
+ * Defines api for theme, can be used to set header logo and title.
+ */
+export class GrThemeApi {
+ constructor(private readonly plugin: PluginApi) {}
+
+ setHeaderLogoAndTitle(logoUrl: string, title: string) {
+ this.plugin.hook('header-title', {replace: true}).onAttached(element => {
+ const customHeader: GrCustomPluginHeader = document.createElement(
+ 'gr-custom-plugin-header'
+ );
+ customHeader.logoUrl = logoUrl;
+ customHeader.title = title;
+ element.appendChild(customHeader);
+ });
+ }
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
index a0d8d13e..4d75138 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.ts
@@ -73,7 +73,7 @@
}
@property({
- computed: 'computeAriaDisable(disabled, loading)',
+ computed: 'computeAriaDisabled(disabled, loading)',
reflectToAttribute: true,
type: Boolean,
})
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
deleted file mode 100644
index 9bef3a2..0000000
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 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 {getBaseUrl} from '../../../utils/url-util.js';
-
-export const PRELOADED_PROTOCOL = 'preloaded:';
-export const PLUGIN_LOADING_TIMEOUT_MS = 10000;
-
-let _restAPI;
-export function getRestAPI() {
- if (!_restAPI) {
- _restAPI = document.createElement('gr-rest-api-interface');
- }
- return _restAPI;
-}
-
-/**
- * Retrieves the name of the plugin base on the url.
- *
- * @param {string|URL} url
- */
-export function getPluginNameFromUrl(url) {
- if (!(url instanceof URL)) {
- try {
- url = new URL(url);
- } catch (e) {
- console.warn(e);
- return null;
- }
- }
- if (url.protocol === PRELOADED_PROTOCOL) {
- return url.pathname;
- }
- const base = getBaseUrl();
- let pathname = url.pathname.replace(base, '');
- // Load from ASSETS_PATH
- if (window.ASSETS_PATH && url.href.includes(window.ASSETS_PATH)) {
- pathname = url.href.replace(window.ASSETS_PATH, '');
- }
- // Site theme is server from predefined path.
- if ([
- '/static/gerrit-theme.html',
- '/static/gerrit-theme.js',
- ].includes(pathname)) {
- return 'gerrit-theme';
- } else if (!pathname.startsWith('/plugins')) {
- console.warn('Plugin not being loaded from /plugins base path:',
- url.href, '— Unable to determine name.');
- return null;
- }
-
- // Pathname should normally look like this:
- // /plugins/PLUGINNAME/static/SCRIPTNAME.html
- // Or, for app/samples:
- // /plugins/PLUGINNAME.html
- // TODO(taoalpha): guard with a regex
- return pathname.split('/')[2].split('.')[0];
-}
-
-// TODO(taoalpha): to be deprecated.
-export function send(method, url, opt_callback, opt_payload) {
- return getRestAPI().send(method, url, opt_payload)
- .then(response => {
- if (response.status < 200 || response.status >= 300) {
- return response.text().then(text => {
- if (text) {
- return Promise.reject(new Error(text));
- } else {
- return Promise.reject(new Error(response.status));
- }
- });
- } else {
- return getRestAPI().getResponseObject(response);
- }
- })
- .then(response => {
- if (opt_callback) {
- opt_callback(response);
- }
- return response;
- });
-}
-
-// TEST only methods / properties
-
-export function testOnly_resetInternalState() {
- _restAPI = undefined;
-}
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
new file mode 100644
index 0000000..2b60e94
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
@@ -0,0 +1,113 @@
+/**
+ * @license
+ * Copyright (C) 2019 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 {getBaseUrl} from '../../../utils/url-util';
+import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
+
+export const PRELOADED_PROTOCOL = 'preloaded:';
+export const PLUGIN_LOADING_TIMEOUT_MS = 10000;
+
+let _restAPI: RestApiService | undefined;
+export function getRestAPI() {
+ if (!_restAPI) {
+ _restAPI = (document.createElement(
+ 'gr-rest-api-interface'
+ ) as unknown) as RestApiService;
+ }
+ return _restAPI;
+}
+
+/**
+ * Retrieves the name of the plugin base on the url.
+ *
+ * @param {string|URL} url
+ */
+export function getPluginNameFromUrl(url: URL | string) {
+ if (!(url instanceof URL)) {
+ try {
+ url = new URL(url);
+ } catch (e) {
+ console.warn(e);
+ return null;
+ }
+ }
+ if (url.protocol === PRELOADED_PROTOCOL) {
+ return url.pathname;
+ }
+ const base = getBaseUrl();
+ let pathname = url.pathname.replace(base, '');
+ // Load from ASSETS_PATH
+ if (window.ASSETS_PATH && url.href.includes(window.ASSETS_PATH)) {
+ pathname = url.href.replace(window.ASSETS_PATH, '');
+ }
+ // Site theme is server from predefined path.
+ if (
+ ['/static/gerrit-theme.html', '/static/gerrit-theme.js'].includes(pathname)
+ ) {
+ return 'gerrit-theme';
+ } else if (!pathname.startsWith('/plugins')) {
+ console.warn(
+ 'Plugin not being loaded from /plugins base path:',
+ url.href,
+ '— Unable to determine name.'
+ );
+ return null;
+ }
+
+ // Pathname should normally look like this:
+ // /plugins/PLUGINNAME/static/SCRIPTNAME.html
+ // Or, for app/samples:
+ // /plugins/PLUGINNAME.html
+ // TODO(taoalpha): guard with a regex
+ return pathname.split('/')[2].split('.')[0];
+}
+
+// TODO(taoalpha): to be deprecated.
+export function send(
+ method: string,
+ url: string,
+ opt_callback?: (response: unknown) => void,
+ opt_payload?: unknown
+) {
+ return getRestAPI()
+ .send(method, url, opt_payload)
+ .then(response => {
+ if (response.status < 200 || response.status >= 300) {
+ return response.text().then((text: string | undefined) => {
+ if (text) {
+ return Promise.reject(new Error(text));
+ } else {
+ return Promise.reject(new Error(`${response.status}`));
+ }
+ });
+ } else {
+ return getRestAPI().getResponseObject(response);
+ }
+ })
+ .then(response => {
+ if (opt_callback) {
+ opt_callback(response);
+ }
+ return response;
+ });
+}
+
+// TEST only methods / properties
+
+export function testOnly_resetInternalState() {
+ _restAPI = undefined;
+}
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
deleted file mode 100644
index 083b802..0000000
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ /dev/null
@@ -1,130 +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 {IronOverlayBehaviorImpl} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.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-overlay_html.js';
-import {IronOverlayMixin} from '../../../mixins/iron-overlay-mixin/iron-overlay-mixin.js';
-
-const AWAIT_MAX_ITERS = 10;
-const AWAIT_STEP = 5;
-const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
-
-/**
- * @extends PolymerElement
- */
-class GrOverlay extends IronOverlayMixin(GestureEventListeners(
- LegacyElementMixin(PolymerElement))) {
- static get template() { return htmlTemplate; }
-
- static get is() { return 'gr-overlay'; }
- /**
- * Fired when a fullscreen overlay is closed
- *
- * @event fullscreen-overlay-closed
- */
-
- /**
- * Fired when an overlay is opened in full screen mode
- *
- * @event fullscreen-overlay-opened
- */
-
- static get properties() {
- return {
- _fullScreenOpen: {
- type: Boolean,
- value: false,
- },
- };
- }
-
- /** @override */
- created() {
- this._boundHandleClose = () => this.close();
- super.created();
- this.addEventListener('iron-overlay-closed',
- () => this._overlayClosed());
- this.addEventListener('iron-overlay-cancelled',
- () => this._overlayClosed());
- }
-
- open(...args) {
- window.addEventListener('popstate', this._boundHandleClose);
- return new Promise((resolve, reject) => {
- IronOverlayBehaviorImpl.open.apply(this, args);
- if (this._isMobile()) {
- this.dispatchEvent(new CustomEvent('fullscreen-overlay-opened', {
- composed: true, bubbles: true,
- }));
- this._fullScreenOpen = true;
- }
- this._awaitOpen(resolve, reject);
- });
- }
-
- _isMobile() {
- return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`);
- }
-
- // called after iron-overlay is closed. Does not actually close the overlay
- _overlayClosed() {
- window.removeEventListener('popstate', this._boundHandleClose);
- if (this._fullScreenOpen) {
- this.dispatchEvent(new CustomEvent('fullscreen-overlay-closed', {
- composed: true, bubbles: true,
- }));
- this._fullScreenOpen = false;
- }
- }
-
- /**
- * Override the focus stops that iron-overlay-behavior tries to find.
- */
- setFocusStops(stops) {
- this.__firstFocusableNode = stops.start;
- this.__lastFocusableNode = stops.end;
- }
-
- /**
- * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
- * opening. Eventually replace with a direct way to listen to the overlay.
- */
- _awaitOpen(fn, reject) {
- let iters = 0;
- const step = () => {
- this.async(() => {
- if (this.style.display !== 'none') {
- fn.call(this);
- } else if (iters++ < AWAIT_MAX_ITERS) {
- step.call(this);
- } else {
- reject(new Error('gr-overlay _awaitOpen failed to resolve'));
- }
- }, AWAIT_STEP);
- };
- step.call(this);
- }
-
- _id() {
- return this.getAttribute('id') || 'global';
- }
-}
-
-customElements.define(GrOverlay.is, GrOverlay);
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
new file mode 100644
index 0000000..aabbac1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
@@ -0,0 +1,148 @@
+/**
+ * @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 '../../../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-overlay_html';
+import {IronOverlayMixin} from '../../../mixins/iron-overlay-mixin/iron-overlay-mixin';
+import {customElement, property} from '@polymer/decorators';
+
+const AWAIT_MAX_ITERS = 10;
+const AWAIT_STEP = 5;
+const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'gr-overlay': GrOverlay;
+ }
+}
+
+@customElement('gr-overlay')
+export class GrOverlay extends IronOverlayMixin(
+ GestureEventListeners(LegacyElementMixin(PolymerElement))
+) {
+ static get template() {
+ return htmlTemplate;
+ }
+
+ /**
+ * Fired when a fullscreen overlay is closed
+ *
+ * @event fullscreen-overlay-closed
+ */
+
+ /**
+ * Fired when an overlay is opened in full screen mode
+ *
+ * @event fullscreen-overlay-opened
+ */
+
+ @property({type: Boolean})
+ private _fullScreenOpen = false;
+
+ private _boundHandleClose: () => void = () => super.close();
+
+ private focusableNodes: Node[] | undefined;
+
+ get _focusableNodes() {
+ if (this.focusableNodes) {
+ return this.focusableNodes;
+ }
+ return super._focusableNodes;
+ }
+
+ /** @override */
+ created() {
+ super.created();
+ this.addEventListener('iron-overlay-closed', () => this._overlayClosed());
+ this.addEventListener('iron-overlay-cancelled', () =>
+ this._overlayClosed()
+ );
+ }
+
+ open() {
+ window.addEventListener('popstate', this._boundHandleClose);
+ return new Promise((resolve, reject) => {
+ super.open.apply(this);
+ if (this._isMobile()) {
+ this.dispatchEvent(
+ new CustomEvent('fullscreen-overlay-opened', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ this._fullScreenOpen = true;
+ }
+ this._awaitOpen(resolve, reject);
+ });
+ }
+
+ _isMobile() {
+ return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`);
+ }
+
+ // called after iron-overlay is closed. Does not actually close the overlay
+ _overlayClosed() {
+ window.removeEventListener('popstate', this._boundHandleClose);
+ if (this._fullScreenOpen) {
+ this.dispatchEvent(
+ new CustomEvent('fullscreen-overlay-closed', {
+ composed: true,
+ bubbles: true,
+ })
+ );
+ this._fullScreenOpen = false;
+ }
+ }
+
+ /**
+ * Override the focus stops that iron-overlay-behavior tries to find.
+ */
+ setFocusStops(stops: GrOverlayStops) {
+ this.focusableNodes = [stops.start, stops.end];
+ }
+
+ /**
+ * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually
+ * opening. Eventually replace with a direct way to listen to the overlay.
+ */
+ _awaitOpen(fn: (this: GrOverlay) => void, reject: (error: Error) => void) {
+ let iters = 0;
+ const step = () => {
+ this.async(() => {
+ if (this.style.display !== 'none') {
+ fn.call(this);
+ } else if (iters++ < AWAIT_MAX_ITERS) {
+ step.call(this);
+ } else {
+ reject(new Error('gr-overlay _awaitOpen failed to resolve'));
+ }
+ }, AWAIT_STEP);
+ };
+ step.call(this);
+ }
+
+ _id() {
+ return this.getAttribute('id') || 'global';
+ }
+}
+
+export interface GrOverlayStops {
+ start: Node;
+ end: Node;
+}
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
index 516877e..5ef17e2 100644
--- a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
+++ b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
@@ -101,7 +101,7 @@
@observe('hasTooltip')
_setupTooltipListeners() {
if (!this.mouseenterHandler) {
- this.mouseenterHandler = () => this._handleShowTooltip();
+ this.mouseenterHandler = this.showHandler;
}
if (!this.hasTooltip) {
@@ -151,7 +151,7 @@
this._tooltip = tooltip;
window.addEventListener('scroll', this.windowScrollHandler);
- this.addEventListener('mouseleave', this.showHandler);
+ this.addEventListener('mouseleave', this.hideHandler);
this.addEventListener('click', this.hideHandler);
}
diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts
index 81db291..c15cee6 100644
--- a/polygerrit-ui/app/types/globals.ts
+++ b/polygerrit-ui/app/types/globals.ts
@@ -27,6 +27,7 @@
text: string,
options: {callback: (text: string, href?: string) => void}
): void;
+ ASSETS_PATH?: string;
}
interface Performance {