Merge changes I48ef685e,Ibe09a864,I8a9ecc8a,I17a20a2b

* changes:
  Allow to specify the author of a new comment of the test API
  Allow to specify the tag of a new comment of the test API
  Explain and test rename/copy behavior of test API of changes
  TreeCreator: Explicitly reject overlapping modifications
This commit is contained in:
Alice Kober-Sotzek
2020-08-19 11:38:24 +00:00
committed by Gerrit Code Review
16 changed files with 368 additions and 28 deletions

View File

@@ -48,11 +48,28 @@ public class FileContentBuilder<T> {
return builder;
}
/** Deletes the file. */
public T delete() {
modificationToBuilderAdder.accept(new DeleteFileModification(filePath));
return builder;
}
/**
* Renames the file while keeping its content.
*
* <p>If you want to both rename the file and adjust its content, delete the old path via {@link
* #delete()} and provide the desired content for the new path via {@link #content(String)}. If
* you use that approach, make sure to use a new content which is similar enough to the old (at
* least 60% line similarity) as otherwise Gerrit/Git won't identify it as a rename.
*
* <p>To create copied files, you need to go even one step further. Also rename the file you copy
* at the same time (-> delete old path + add two paths with the old content)! If you also want to
* adjust the content of the copy, you need to also slightly modify the content of the renamed
* file. Adjust the content of the copy slightly more if you want to control which file ends up as
* copy and which as rename (but keep the 60% line similarity threshold in mind).
*
* @param newFilePath new path of the file
*/
public T renameTo(String newFilePath) {
modificationToBuilderAdder.accept(new RenameFileModification(filePath, newFilePath));
return builder;

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.acceptance.testsuite.change;
import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
import com.google.gerrit.acceptance.testsuite.change.TestCommentCreation.CommentSide;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
@@ -30,6 +31,7 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.update.BatchUpdate;
@@ -103,10 +105,9 @@ public class PerPatchsetOperationsImpl implements PerPatchsetOperations {
RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
Timestamp now = TimeUtil.nowTs();
// Use identity of change owner until the API allows to specify the commenter.
IdentifiedUser changeOwner = userFactory.create(changeNotes.getChange().getOwner());
IdentifiedUser author = getAuthor(commentCreation);
CommentAdditionOp commentAdditionOp = new CommentAdditionOp(commentCreation);
try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) {
batchUpdate.setRepository(repository, revWalk, objectInserter);
batchUpdate.addOp(changeNotes.getChangeId(), commentAdditionOp);
batchUpdate.execute();
@@ -115,6 +116,11 @@ public class PerPatchsetOperationsImpl implements PerPatchsetOperations {
}
}
private IdentifiedUser getAuthor(TestCommentCreation commentCreation) {
Account.Id authorId = commentCreation.author().orElse(changeNotes.getChange().getOwner());
return userFactory.create(authorId);
}
private class CommentAdditionOp implements BatchUpdateOp {
private String createdCommentUuid;
private final TestCommentCreation commentCreation;
@@ -126,7 +132,10 @@ public class PerPatchsetOperationsImpl implements PerPatchsetOperations {
@Override
public boolean updateChange(ChangeContext context) throws Exception {
HumanComment comment = toNewComment(context, commentCreation);
context.getUpdate(patchsetId).putComment(HumanComment.Status.PUBLISHED, comment);
ChangeUpdate changeUpdate = context.getUpdate(patchsetId);
changeUpdate.putComment(HumanComment.Status.PUBLISHED, comment);
// Only the tag set on the ChangeUpdate matters. The tag field of HumanComment is ignored.
commentCreation.tag().ifPresent(changeUpdate::setTag);
createdCommentUuid = comment.key.uuid;
return true;
}

View File

@@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
import com.google.gerrit.acceptance.testsuite.change.TestRange.Position;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Patch;
import java.util.Optional;
import java.util.function.Function;
@@ -41,6 +42,10 @@ public abstract class TestCommentCreation {
public abstract Optional<String> parentUuid();
public abstract Optional<String> tag();
public abstract Optional<Account.Id> author();
abstract ThrowingFunction<TestCommentCreation, String> commentCreator();
public static TestCommentCreation.Builder builder(
@@ -155,6 +160,12 @@ public abstract class TestCommentCreation {
*/
public abstract Builder parentUuid(String parentUuid);
/** Tag to attach to the comment. */
public abstract Builder tag(String value);
/** Author of the comment. Must be an existing user account. */
public abstract Builder author(Account.Id accountId);
abstract TestCommentCreation.Builder commentCreator(
ThrowingFunction<TestCommentCreation, String> commentCreator);

View File

@@ -0,0 +1,50 @@
// 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.extensions.common.testing;
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.Subject;
import com.google.gerrit.extensions.common.AccountInfo;
/** A Truth subject for {@link AccountInfo} instances. */
public class AccountInfoSubject extends Subject {
private final AccountInfo accountInfo;
public static AccountInfoSubject assertThat(AccountInfo accountInfo) {
return assertAbout(accounts()).that(accountInfo);
}
public static Factory<AccountInfoSubject, AccountInfo> accounts() {
return AccountInfoSubject::new;
}
private AccountInfoSubject(FailureMetadata metadata, AccountInfo accountInfo) {
super(metadata, accountInfo);
this.accountInfo = accountInfo;
}
public IntegerSubject id() {
return check("id").that(accountInfo()._accountId);
}
private AccountInfo accountInfo() {
isNotNull();
return accountInfo;
}
}

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.common.testing;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.gerrit.extensions.common.testing.AccountInfoSubject.accounts;
import static com.google.gerrit.extensions.common.testing.RangeSubject.ranges;
import com.google.common.truth.BooleanSubject;
@@ -50,6 +51,10 @@ public class CommentInfoSubject extends Subject {
this.commentInfo = commentInfo;
}
public StringSubject uuid() {
return check("id").that(commentInfo().id);
}
public IntegerSubject patchSet() {
return check("patchSet").that(commentInfo().patchSet);
}
@@ -86,6 +91,14 @@ public class CommentInfoSubject extends Subject {
return check("inReplyTo").that(commentInfo().inReplyTo);
}
public AccountInfoSubject author() {
return check("author").about(accounts()).that(commentInfo().author);
}
public StringSubject tag() {
return check("tag").that(commentInfo().tag);
}
private CommentInfo commentInfo() {
isNotNull();
return commentInfo;

View File

@@ -19,6 +19,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.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteStreams;
import com.google.gerrit.extensions.restapi.RawInput;
@@ -55,8 +56,8 @@ public class ChangeFileContentModification implements TreeModification {
}
@Override
public String getFilePath() {
return filePath;
public ImmutableSet<String> getFilePaths() {
return ImmutableSet.of(filePath);
}
@VisibleForTesting

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.server.edit.tree;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -38,7 +39,7 @@ public class DeleteFileModification implements TreeModification {
}
@Override
public String getFilePath() {
return filePath;
public ImmutableSet<String> getFilePaths() {
return ImmutableSet.of(filePath);
}
}

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.server.edit.tree;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -57,7 +58,7 @@ public class RenameFileModification implements TreeModification {
}
@Override
public String getFilePath() {
return newFilePath;
public ImmutableSet<String> getFilePaths() {
return ImmutableSet.of(currentFilePath, newFilePath);
}
}

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.server.edit.tree;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@@ -62,7 +63,7 @@ public class RestoreFileModification implements TreeModification {
}
@Override
public String getFilePath() {
return filePath;
public ImmutableSet<String> getFilePaths() {
return ImmutableSet.of(filePath);
}
}

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.edit.tree;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
@@ -56,7 +57,11 @@ public class TreeCreator {
/**
* Apply modifications to the tree which is taken as a basis. If this method is called multiple
* times, the modifications are applied subsequently in exactly the order they were provided.
* times, the modifications are applied subsequently in exactly the order they were provided
* (though JGit applies some internal optimizations which involve sorting, too).
*
* <p><strong>Beware:</strong> All provided {@link TreeModification}s (even from previous calls of
* this method) must touch different file paths!
*
* @param treeModifications modifications which should be applied to the base tree
*/
@@ -75,10 +80,32 @@ public class TreeCreator {
* @throws IOException if problems arise when accessing the repository
*/
public ObjectId createNewTreeAndGetId(Repository repository) throws IOException {
ensureTreeModificationsDoNotTouchSameFiles();
DirCache newTree = createNewTree(repository);
return writeAndGetId(repository, newTree);
}
private void ensureTreeModificationsDoNotTouchSameFiles() {
// The current implementation of TreeCreator doesn't properly support modifications which touch
// the same files even if they are provided in a logical order. According to JGit's
// documentation, DirCache applies some internal sorting to optimize the index modifications.
// The internal sorting doesn't seem to be the only issue, though. Even applying the
// modifications in batches within different, subsequent DirCaches just held in memory didn't
// seem to work. We might need to fully write each batch to disk before creating the next.
ImmutableList<String> filePaths =
treeModifications.stream()
.flatMap(treeModification -> treeModification.getFilePaths().stream())
.collect(toImmutableList());
long distinctFilePathNum = filePaths.stream().distinct().count();
if (filePaths.size() != distinctFilePathNum) {
throw new IllegalStateException(
String.format(
"TreeModifications must not refer to the same file paths. This would have"
+ " unexpected/wrong behavior! Found file paths: %s.",
filePaths));
}
}
private DirCache createNewTree(Repository repository) throws IOException {
DirCache newTree = readBaseTree(repository);
List<DirCacheEditor.PathEdit> pathEdits = getPathEdits(repository);

View File

@@ -14,8 +14,8 @@
package com.google.gerrit.server.edit.tree;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -42,12 +42,10 @@ public interface TreeModification {
throws IOException;
/**
* Indicates a file path which is affected by this {@code TreeModification}. If the modification
* refers to several file paths (e.g. renaming a file), returning either of them is appropriate as
* long as the returned value is deterministic.
* Indicates all file paths affected by this {@code TreeModification}. If the modification refers
* to several file paths (e.g. renaming a file), all of them must be returned.
*
* @return an affected file path
* @return all affected file paths
*/
@VisibleForTesting
String getFilePath();
ImmutableSet<String> getFilePaths();
}