Merge "Fix SEND_REPLY timer."
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 844f4c6..8aa5c7f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2916,6 +2916,11 @@
The custom keyed values to add or remove must be provided in the request body
inside a link:#custom-keyed-values-input[CustomKeyedValuesInput] entity.
+Note that custom keyed values are expected to be small in both key and value.
+A typical use-case would be storing the ID to some external system, in which
+case the key would be something unique to that system and the value would be
+the ID.
+
.Request
----
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/custom_keyed_values HTTP/1.0
diff --git a/java/com/google/gerrit/server/git/TagSet.java b/java/com/google/gerrit/server/git/TagSet.java
index a528c8f..bffc479 100644
--- a/java/com/google/gerrit/server/git/TagSet.java
+++ b/java/com/google/gerrit/server/git/TagSet.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.Preconditions.checkNotNull;
+
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSet;
@@ -41,6 +43,7 @@
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
import org.roaringbitmap.RoaringBitmap;
@@ -209,6 +212,7 @@
try (TagWalk rw = new TagWalk(git)) {
rw.setRetainBody(false);
+ RevFlag isTag = rw.newFlag("tag");
for (Ref ref :
git.getRefDatabase()
.getRefsByPrefixWithExclusions(RefDatabase.ALL, SKIPPABLE_REF_PREFIXES)) {
@@ -218,9 +222,9 @@
} else if (isTag(ref)) {
// For a tag, remember where it points to.
try {
- addTag(rw, git.getRefDatabase().peel(ref));
+ addTag(rw, git.getRefDatabase().peel(ref), isTag);
} catch (IOException e) {
- addTag(rw, ref);
+ addTag(rw, ref, isTag);
}
} else {
@@ -229,17 +233,10 @@
}
}
- // Traverse the complete history. Copy any flags from a commit to
- // all of its ancestors. This automatically updates any Tag object
- // as the TagCommit and the stored Tag object share the same
- // underlying bit set.
+ // Traverse the complete history and propagate reachability to parents.
TagCommit c;
while ((c = (TagCommit) rw.next()) != null) {
- RoaringBitmap mine = c.refFlags;
- int pCnt = c.getParentCount();
- for (int pIdx = 0; pIdx < pCnt; pIdx++) {
- ((TagCommit) c.getParent(pIdx)).refFlags.or(mine);
- }
+ c.propagateReachabilityToParents(isTag);
}
} catch (IOException e) {
logger.atWarning().withCause(e).log("Error building tags for repository %s", projectName);
@@ -356,9 +353,7 @@
refs.putAll(old.refs);
for (Tag srcTag : old.tags) {
- RoaringBitmap mine = new RoaringBitmap();
- mine.or(srcTag.refFlags);
- tags.add(new Tag(srcTag, mine));
+ tags.add(new Tag(srcTag));
}
for (TagMatcher.LostRef lost : m.lostRefs) {
@@ -369,7 +364,7 @@
}
}
- private void addTag(TagWalk rw, Ref ref) {
+ private void addTag(TagWalk rw, Ref ref, RevFlag isTag) {
ObjectId id = ref.getPeeledObjectId();
if (id == null) {
id = ref.getObjectId();
@@ -378,7 +373,12 @@
if (!tags.contains(id)) {
RoaringBitmap flags;
try {
- flags = ((TagCommit) rw.parseCommit(id)).refFlags;
+ TagCommit commit = ((TagCommit) rw.parseCommit(id));
+ commit.add(isTag);
+ if (commit.refFlags == null) {
+ commit.refFlags = new RoaringBitmap();
+ }
+ flags = commit.refFlags;
} catch (IncorrectObjectTypeException notCommit) {
flags = new RoaringBitmap();
} catch (IOException e) {
@@ -395,6 +395,9 @@
rw.markStart(commit);
int flag = refs.size();
+ if (commit.refFlags == null) {
+ commit.refFlags = new RoaringBitmap();
+ }
commit.refFlags.add(flag);
refs.put(ref.getName(), new CachedRef(ref, flag));
} catch (IncorrectObjectTypeException notCommit) {
@@ -432,8 +435,13 @@
// RoaringBitmap in TagCommit.refFlags.
@VisibleForTesting final RoaringBitmap refFlags;
+ Tag(Tag src) {
+ this(src, src.refFlags.clone());
+ }
+
Tag(AnyObjectId id, RoaringBitmap flags) {
super(id);
+ checkNotNull(flags);
this.refFlags = flags;
}
@@ -485,13 +493,50 @@
}
// TODO(hanwen): this would be better named as CommitWithReachability, as it also holds non-tags.
+ // However, non-tags will have a null refFlags field.
private static final class TagCommit extends RevCommit {
/** CachedRef.flag => isVisible, indicating if this commit is reachable from the ref. */
- final RoaringBitmap refFlags;
+ RoaringBitmap refFlags;
TagCommit(AnyObjectId id) {
super(id);
- refFlags = new RoaringBitmap();
+ }
+
+ /**
+ * Copy any flags from this commit to all of its ancestors.
+ *
+ * <p>Do not maintain a reference to the flags on non-tag commits after copying their flags to
+ * their ancestors. The flag copying automatically updates any Tag object as the TagCommit and
+ * the stored Tag object share the same underlying RoaringBitmap.
+ *
+ * @param isTag {@code RevFlag} indicating if this TagCommit is a tag
+ */
+ void propagateReachabilityToParents(RevFlag isTag) {
+ RoaringBitmap mine = refFlags;
+ if (mine != null) {
+ boolean canMoveBitmap = false;
+ if (!has(isTag)) {
+ refFlags = null;
+ canMoveBitmap = true;
+ }
+ int pCnt = getParentCount();
+ for (int pIdx = 0; pIdx < pCnt; pIdx++) {
+ TagCommit commit = (TagCommit) getParent(pIdx);
+ RoaringBitmap parentFlags = commit.refFlags;
+ if (parentFlags == null) {
+ if (canMoveBitmap) {
+ // This commit is not itself a Tag, so in order to reduce cloning overhead, migrate
+ // its refFlags object to its first parent with null refFlags
+ commit.refFlags = mine;
+ canMoveBitmap = false;
+ } else {
+ commit.refFlags = mine.clone();
+ }
+ } else {
+ parentFlags.or(mine);
+ }
+ }
+ }
}
}
}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 6d4d74d..14e40bd 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -23,10 +23,12 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.DraftCommentsReader;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.change.HashtagsUtil;
import com.google.gerrit.server.index.change.ChangeField;
+import com.google.inject.ImplementedBy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -61,12 +63,25 @@
return new ChangeIndexPredicate(ChangeField.COMMENTBY_SPEC, id.toString());
}
+ @ImplementedBy(IndexEditByPredicateProvider.class)
+ public interface EditByPredicateProvider {
+
+ /**
+ * Returns a predicate that matches changes where the provided {@link
+ * com.google.gerrit.entities.Account.Id} has a pending change edit.
+ */
+ Predicate<ChangeData> editBy(Account.Id id) throws QueryParseException;
+ }
+
/**
- * Returns a predicate that matches changes where the provided {@link
- * com.google.gerrit.entities.Account.Id} has a pending change edit.
+ * A default implementation of {@link EditByPredicateProvider}, based on th {@link
+ * ChangeField#EDITBY_SPEC} index field.
*/
- public static Predicate<ChangeData> editBy(Account.Id id) {
- return new ChangeIndexPredicate(ChangeField.EDITBY_SPEC, id.toString());
+ public static class IndexEditByPredicateProvider implements EditByPredicateProvider {
+ @Override
+ public Predicate<ChangeData> editBy(Account.Id id) {
+ return new ChangeIndexPredicate(ChangeField.EDITBY_SPEC, id.toString());
+ }
}
/**
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index b3fa087..f8a4a99 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -89,6 +89,7 @@
import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ChildProjects;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangePredicates.EditByPredicateProvider;
import com.google.gerrit.server.query.change.PredicateArgs.ValOp;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.submit.SubmitDryRun;
@@ -284,6 +285,8 @@
private final Provider<CurrentUser> self;
+ private final EditByPredicateProvider editByPredicateProvider;
+
@Inject
@VisibleForTesting
public Arguments(
@@ -318,7 +321,8 @@
ExperimentFeatures experimentFeatures,
HasOperandAliasConfig hasOperandAliasConfig,
ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
- PluginSetContext<SubmitRule> submitRules) {
+ PluginSetContext<SubmitRule> submitRules,
+ EditByPredicateProvider editByPredicateProvider) {
this(
queryProvider,
rewriter,
@@ -352,7 +356,8 @@
experimentFeatures,
hasOperandAliasConfig,
changeIsVisbleToPredicateFactory,
- submitRules);
+ submitRules,
+ editByPredicateProvider);
}
private Arguments(
@@ -388,7 +393,8 @@
ExperimentFeatures experimentFeatures,
HasOperandAliasConfig hasOperandAliasConfig,
ChangeIsVisibleToPredicate.Factory changeIsVisbleToPredicateFactory,
- PluginSetContext<SubmitRule> submitRules) {
+ PluginSetContext<SubmitRule> submitRules,
+ EditByPredicateProvider editByPredicateProvider) {
this.queryProvider = queryProvider;
this.rewriter = rewriter;
this.opFactories = opFactories;
@@ -422,6 +428,7 @@
this.experimentFeatures = experimentFeatures;
this.hasOperandAliasConfig = hasOperandAliasConfig;
this.submitRules = submitRules;
+ this.editByPredicateProvider = editByPredicateProvider;
}
public Arguments asUser(CurrentUser otherUser) {
@@ -458,7 +465,8 @@
experimentFeatures,
hasOperandAliasConfig,
changeIsVisbleToPredicateFactory,
- submitRules);
+ submitRules,
+ editByPredicateProvider);
}
Arguments asUser(Account.Id otherId) {
@@ -646,7 +654,7 @@
}
if ("edit".equalsIgnoreCase(value)) {
- return ChangePredicates.editBy(self());
+ return this.args.editByPredicateProvider.editBy(self());
}
if ("attention".equalsIgnoreCase(value)) {
@@ -1847,7 +1855,7 @@
}
/** Returns {@link com.google.gerrit.entities.Account.Id} of the identified calling user. */
- public Account.Id self() throws QueryParseException {
+ private Account.Id self() throws QueryParseException {
return args.getIdentifiedUser().getAccountId();
}
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index 3e471fb..5993c76 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.index.query.Predicate.and;
import static com.google.gerrit.index.query.Predicate.not;
import static com.google.gerrit.index.query.Predicate.or;
+import static com.google.gerrit.server.query.change.ChangePredicates.EditByPredicateProvider;
import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
import com.google.common.annotations.VisibleForTesting;
@@ -35,6 +36,7 @@
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.query.InternalQuery;
import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -75,8 +77,8 @@
return ChangeStatusPredicate.forStatus(status);
}
- private static Predicate<ChangeData> editBy(Account.Id accountId) {
- return ChangePredicates.editBy(accountId);
+ private Predicate<ChangeData> editBy(Account.Id accountId) throws QueryParseException {
+ return editByPredicateProvider.editBy(accountId);
}
private static Predicate<ChangeData> commit(String id) {
@@ -85,6 +87,7 @@
private final ChangeData.Factory changeDataFactory;
private final ChangeNotes.Factory notesFactory;
+ private final EditByPredicateProvider editByPredicateProvider;
@Inject
InternalChangeQuery(
@@ -92,10 +95,12 @@
ChangeIndexCollection indexes,
IndexConfig indexConfig,
ChangeData.Factory changeDataFactory,
- ChangeNotes.Factory notesFactory) {
+ ChangeNotes.Factory notesFactory,
+ EditByPredicateProvider editByPredicateProvider) {
super(queryProcessor, indexes, indexConfig);
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
+ this.editByPredicateProvider = editByPredicateProvider;
}
public List<ChangeData> byKey(Change.Key key) {
@@ -221,7 +226,7 @@
return query(and(ChangePredicates.exactTopic(topic), open()));
}
- public List<ChangeData> byOpenEditByUser(Account.Id accountId) {
+ public List<ChangeData> byOpenEditByUser(Account.Id accountId) throws QueryParseException {
return query(editBy(accountId));
}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteAccount.java b/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
index 9b4c0a6..0831ff9 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteAccount.java
@@ -26,6 +26,7 @@
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.gpg.PublicKeyStoreUtil;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
@@ -166,7 +167,7 @@
}
}
- private void deleteChangeEdits(Account.Id accountId) throws IOException {
+ private void deleteChangeEdits(Account.Id accountId) throws IOException, QueryParseException {
// Note: in case of a stale index, the results of this query might be incomplete.
List<ChangeData> changesWithEdits = queryProvider.get().byOpenEditByUser(accountId);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 8b99e1c..677279e 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -175,7 +175,8 @@
null,
null,
null,
- null);
+ null,
+ Optional.empty());
}
/**
@@ -206,7 +207,17 @@
throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
ConfigInvalidException, NoSuchProjectException {
return cherryPick(
- sourceChange, project, sourceCommit, input, dest, TimeUtil.now(), null, null, null, null);
+ sourceChange,
+ project,
+ sourceCommit,
+ input,
+ dest,
+ TimeUtil.now(),
+ null,
+ null,
+ null,
+ null,
+ Optional.empty());
}
/**
@@ -228,6 +239,9 @@
* @param idForNewChange The ID that the new change of the cherry pick will have. If provided and
* the cherry-pick doesn't result in creating a new change, then
* InvalidChangeOperationException is thrown.
+ * @param verifiedBaseCommit - base commit for the cherry-pick, which is guaranteed to be
+ * associated with exactly one change and belong to a {@code dest} branch. This is currently
+ * only used when this base commit was created in the same API call.
* @return Result object that describes the cherry pick.
* @throws IOException Unable to open repository or read from the database.
* @throws InvalidChangeOperationException Parent or branch don't exist, or two changes with same
@@ -248,7 +262,8 @@
@Nullable Change.Id revertedChange,
@Nullable ObjectId changeIdForNewChange,
@Nullable Change.Id idForNewChange,
- @Nullable Boolean workInProgress)
+ @Nullable Boolean workInProgress,
+ Optional<RevCommit> verifiedBaseCommit)
throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
ConfigInvalidException, NoSuchProjectException {
IdentifiedUser identifiedUser = user.get();
@@ -266,8 +281,9 @@
}
RevCommit baseCommit =
- CommitUtil.getBaseCommit(
- project.get(), queryProvider.get(), revWalk, destRef, input.base);
+ verifiedBaseCommit.orElse(
+ CommitUtil.getBaseCommit(
+ project.get(), queryProvider.get(), revWalk, destRef, input.base));
CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceCommit);
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 691fc75..3851e82 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -86,6 +86,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -310,6 +311,13 @@
cherryPickInput.message = revertInput.message;
ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId());
+ RevCommit baseCommit = null;
+ if (cherryPickInput.base != null) {
+ try (Repository git = repoManager.openRepository(changeNotes.getProjectName());
+ RevWalk revWalk = new RevWalk(git.newObjectReader())) {
+ baseCommit = revWalk.parseCommit(ObjectId.fromString(cherryPickInput.base));
+ }
+ }
try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.now())) {
bu.setNotify(
@@ -323,7 +331,8 @@
generatedChangeId,
cherryPickRevertChangeId,
timestamp,
- revertInput.workInProgress));
+ revertInput.workInProgress,
+ baseCommit));
if (!revertInput.workInProgress) {
commitUtil.addChangeRevertedNotificationOps(
bu, changeNotes.getChangeId(), cherryPickRevertChangeId, generatedChangeId.name());
@@ -548,18 +557,21 @@
private final Change.Id cherryPickRevertChangeId;
private final Instant timestamp;
private final boolean workInProgress;
+ private final RevCommit baseCommit;
CreateCherryPickOp(
ObjectId revCommitId,
ObjectId computedChangeId,
Change.Id cherryPickRevertChangeId,
Instant timestamp,
- Boolean workInProgress) {
+ Boolean workInProgress,
+ RevCommit baseCommit) {
this.revCommitId = revCommitId;
this.computedChangeId = computedChangeId;
this.cherryPickRevertChangeId = cherryPickRevertChangeId;
this.timestamp = timestamp;
this.workInProgress = workInProgress;
+ this.baseCommit = baseCommit;
}
@Override
@@ -577,7 +589,8 @@
change.getId(),
computedChangeId,
cherryPickRevertChangeId,
- workInProgress);
+ workInProgress,
+ Optional.ofNullable(baseCommit));
// save the commit as base for next cherryPick of that branch
cherryPickInput.base =
changeNotesFactory
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index 7d3fe98..97b08f0 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -77,7 +77,7 @@
bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input));
if (change.getRevertOf() != null) {
commitUtil.addChangeRevertedNotificationOps(
- bu, change.getRevertOf(), change.getId(), change.getKey().get());
+ bu, change.getRevertOf(), change.getId(), change.getKey().get().substring(1));
}
bu.execute();
return Response.ok();
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index c920843..3a835b7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -448,7 +448,7 @@
List<ChangeMessageInfo> sourceMessages =
new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
assertThat(sourceMessages).hasSize(4);
- String expectedMessage = String.format("Created a revert of this change as I%s", changeId);
+ String expectedMessage = String.format("Created a revert of this change as %s", changeId);
assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 4855ba4..7c50e93 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -225,6 +225,12 @@
List<ChangeMessageInfo> sourceMessages =
new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
assertThat(sourceMessages).hasSize(3);
+ // Publishing creates a revert message
+ gApi.changes().id(revertChange.changeId).setReadyForReview();
+ sourceMessages = new ArrayList<>(gApi.changes().id(r.getChangeId()).get().messages);
+ assertThat(sourceMessages).hasSize(4);
+ assertThat(sourceMessages.get(3).message)
+ .isEqualTo("Created a revert of this change as " + revertChange.changeId);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 9e85d8c..c0531e5 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -875,6 +875,47 @@
"refs/tags/new-tag");
}
+ // rcMaster (c1 master master-tag) <- rcBranch (c2 branch branch-tag) <- rcBranch (c2 branch) <-
+ // newcommit1 <- newcommit2 (new-branch)
+ @Test
+ public void uploadPackReachableTagVisibleFromLeafBranch() throws Exception {
+ try (Repository repo = repoManager.openRepository(project)) {
+ // rcBranch (c2 branch) <- newcommit1 (new-branch)
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), testRepo)
+ .setParent(rcBranch)
+ .to("refs/heads/new-branch");
+ r.assertOkStatus();
+ RevCommit branchRc = r.getCommit();
+
+ // rcBranch (c2) <- newcommit1 <- newcommit2 (new-branch)
+ r =
+ pushFactory
+ .create(admin.newIdent(), testRepo)
+ .setParent(branchRc)
+ .to("refs/heads/new-branch");
+ r.assertOkStatus();
+ }
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(deny(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+ .add(deny(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
+ .add(allow(Permission.READ).ref("refs/heads/new-branch").group(REGISTERED_USERS))
+ .update();
+
+ requestScopeOperations.setApiUser(user.id());
+ assertUploadPackRefs(
+ "refs/heads/new-branch",
+ // 'master' and 'branch' branches are not visible but 'master-tag' and 'branch-tag' are
+ // reachable from new-branch (since PushOneCommit always bases changes on each other).
+ "refs/tags/branch-tag",
+ "refs/tags/master-tag");
+ // tree-tag not visible. See comment in subsetOfBranchesVisibleIncludingHead.
+ }
+
// first ls-remote: rcBranch (c2 branch) <- newcommit1 (updated-tag)
// second ls-remote: rcBranch (c2 branch updated-tag)
@Test
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index ea8e0a7..90a9b9d 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -59,6 +59,7 @@
null,
null,
null,
+ null,
null));
}
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
index eef2a44..04887ad 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.ts
@@ -74,8 +74,6 @@
@customElement('gr-admin-view')
export class GrAdminView extends LitElement {
- private account?: AccountDetailInfo;
-
@state()
view?: GerritView;
@@ -459,13 +457,18 @@
async reload() {
try {
this.reloading = true;
+ // There is async barrier inside reload function, we need to clear
+ // subsectionLinks now, because the element might render while waiting for
+ // RestApi responses breaking the invariant that this.view is part of
+ // subsectionLinks if non-empty.
+ this.subsectionLinks = [];
const promises: [Promise<AccountDetailInfo | undefined>, Promise<void>] =
[
this.restApiService.getAccount(),
this.getPluginLoader().awaitPluginsLoaded(),
];
const result = await Promise.all(promises);
- this.account = result[0];
+ const account = result[0];
let options: AdminNavLinksOption | undefined = undefined;
if (this.repoName) {
options = {repoName: this.repoName};
@@ -484,7 +487,7 @@
}
const res = await getAdminLinks(
- this.account,
+ account,
() =>
this.restApiService.getAccountCapabilities().then(capabilities => {
if (!capabilities) {
@@ -501,7 +504,6 @@
: '';
if (!res.expandedSection) {
- this.subsectionLinks = [];
return;
}
this.subsectionLinks = [res.expandedSection]
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
index ee4a179..8cf4e05 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.ts
@@ -111,10 +111,10 @@
assert.isNotOk(element.filteredLinks![0].subsection);
// Groups
- assert.isNotOk(element.filteredLinks![0].subsection);
+ assert.isNotOk(element.filteredLinks![1].subsection);
// Plugins
- assert.isNotOk(element.filteredLinks![0].subsection);
+ assert.isNotOk(element.filteredLinks![2].subsection);
});
test('filteredLinks non admin authenticated', async () => {
@@ -123,7 +123,7 @@
// Repos
assert.isNotOk(element.filteredLinks![0].subsection);
// Groups
- assert.isNotOk(element.filteredLinks![0].subsection);
+ assert.isNotOk(element.filteredLinks![1].subsection);
});
test('filteredLinks non admin unathenticated', async () => {
@@ -262,6 +262,7 @@
test('Needs reload when changing from repo to group', async () => {
element.repoName = 'Test Repo' as RepoName;
+ element.view = GerritView.REPO;
stubRestApi('getAccount').returns(
Promise.resolve({
name: 'test-user',
@@ -278,9 +279,12 @@
const groupId = '1' as GroupId;
element.view = GerritView.GROUP;
element.groupViewState = {groupId, view: GerritView.GROUP};
+ // Check for reload before update. This would normally be done as part of
+ // subscribe method that updates the view/viewState.
+ assert.isTrue(element.needsReload());
+ element.reload();
await element.updateComplete;
- assert.isTrue(element.needsReload());
assert.equal(element.groupId, groupId);
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
index 57f9ee6..3f99416 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.ts
@@ -117,10 +117,12 @@
return;
}
- this.restApiService.getAccountDetails(userId).then(details => {
- this._accountDetails = details ?? undefined;
- this._status = details?.status ?? '';
- });
+ this.restApiService
+ .getAccountDetails(userId, () => {})
+ .then(details => {
+ this._accountDetails = details ?? undefined;
+ this._status = details?.status ?? '';
+ });
}
_computeDetail(
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index 57bb875..aa569ad 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1025,23 +1025,17 @@
}
private actionsChanged() {
- this.hidden =
- Object.keys(this.actions).length === 0 &&
- Object.keys(this.revisionActions).length === 0 &&
- this.additionalActions.length === 0;
this.actionLoadingMessage = '';
this.disabledMenuActions = [];
- if (Object.keys(this.revisionActions).length !== 0) {
- if (!this.revisionActions.download) {
- this.revisionActions = {
- ...this.revisionActions,
- download: DOWNLOAD_ACTION,
- };
- fire(this, 'revision-actions-changed', {
- value: this.revisionActions,
- });
- }
+ if (!this.revisionActions.download) {
+ this.revisionActions = {
+ ...this.revisionActions,
+ download: DOWNLOAD_ACTION,
+ };
+ fire(this, 'revision-actions-changed', {
+ value: this.revisionActions,
+ });
}
if (
!this.actions.includedIn &&
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index ae60449..f083609 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -390,7 +390,7 @@
</gr-copy-clipboard>
</div>
<div class="commitActions">
- <gr-change-actions hidden="" id="actions"> </gr-change-actions>
+ <gr-change-actions id="actions"> </gr-change-actions>
</div>
</div>
<h2 class="assistive-tech-only">Change metadata</h2>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 97b8de3..d6e7336 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -128,7 +128,7 @@
maxDeleted: number;
maxAdditionWidth: number;
maxDeletionWidth: number;
- deletionOffset: number;
+ additionOffset: number;
}
function createDefaultSizeBarLayout(): SizeBarLayout {
@@ -139,7 +139,7 @@
maxDeleted: 0,
maxAdditionWidth: 0,
maxDeletionWidth: 0,
- deletionOffset: 0,
+ additionOffset: 0,
};
}
@@ -463,13 +463,13 @@
}
.added {
color: var(--positive-green-text-color);
- }
- .removed {
- color: var(--negative-red-text-color);
text-align: left;
min-width: 4em;
padding-left: var(--spacing-s);
}
+ .removed {
+ color: var(--negative-red-text-color);
+ }
.drafts {
color: var(--error-foreground);
font-weight: var(--font-weight-bold);
@@ -1321,19 +1321,19 @@
<div class=${this.computeSizeBarsClass(file.__path)} aria-hidden="true">
<svg width="61" height="8">
<rect
- x=${this.computeBarAdditionX(file, sizeBarLayout)}
- y="0"
- height="8"
- fill="var(--positive-green-text-color)"
- width=${this.computeBarAdditionWidth(file, sizeBarLayout)}
- ></rect>
- <rect
- x=${this.computeBarDeletionX(sizeBarLayout)}
+ x=${this.computeBarDeletionX(file, sizeBarLayout)}
y="0"
height="8"
fill="var(--negative-red-text-color)"
width=${this.computeBarDeletionWidth(file, sizeBarLayout)}
></rect>
+ <rect
+ x=${this.computeBarAdditionX(sizeBarLayout)}
+ y="0"
+ height="8"
+ fill="var(--positive-green-text-color)"
+ width=${this.computeBarAdditionWidth(file, sizeBarLayout)}
+ ></rect>
</svg>
</div>
</div>`;
@@ -1348,14 +1348,6 @@
-->
<div class=${this.computeClass('', file.__path)}>
<span
- class="added"
- tabindex="0"
- aria-label=${`${file.lines_inserted} added`}
- ?hidden=${file.binary}
- >
- +${file.lines_inserted}
- </span>
- <span
class="removed"
tabindex="0"
aria-label=${`${file.lines_deleted} removed`}
@@ -1364,6 +1356,14 @@
-${file.lines_deleted}
</span>
<span
+ class="added"
+ tabindex="0"
+ aria-label=${`${file.lines_inserted} added`}
+ ?hidden=${file.binary}
+ >
+ +${file.lines_inserted}
+ </span>
+ <span
class=${ifDefined(this.computeBinaryClass(file.size_delta))}
?hidden=${!file.binary}
>
@@ -1542,19 +1542,19 @@
<div class="total-stats">
<div>
<span
- class="added"
- tabindex="0"
- aria-label="Total ${patchChange.inserted} lines added"
- >
- +${patchChange.inserted}
- </span>
- <span
class="removed"
tabindex="0"
aria-label="Total ${patchChange.deleted} lines removed"
>
-${patchChange.deleted}
</span>
+ <span
+ class="added"
+ tabindex="0"
+ aria-label="Total ${patchChange.inserted} lines added"
+ >
+ +${patchChange.inserted}
+ </span>
</div>
</div>
${when(showDynamicColumns, () =>
@@ -1586,16 +1586,6 @@
<div class="row totalChanges">
<div class="total-stats">
<span
- class="added"
- aria-label="Total bytes inserted: ${deltaInserted}"
- >
- ${deltaInserted}
- ${this.formatPercentage(
- patchChange.total_size,
- patchChange.size_delta_inserted
- )}
- </span>
- <span
class="removed"
aria-label="Total bytes removed: ${deltaDeleted}"
>
@@ -1605,6 +1595,16 @@
patchChange.size_delta_deleted
)}
</span>
+ <span
+ class="added"
+ aria-label="Total bytes inserted: ${deltaInserted}"
+ >
+ ${deltaInserted}
+ ${this.formatPercentage(
+ patchChange.total_size,
+ patchChange.size_delta_inserted
+ )}
+ </span>
</div>
</div>
`;
@@ -2470,7 +2470,7 @@
(SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio;
stats.maxDeletionWidth =
SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth;
- stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH;
+ stats.additionOffset = stats.maxDeletionWidth + SIZE_BAR_GAP_WIDTH;
}
return stats;
}
@@ -2498,9 +2498,8 @@
* Get the x-offset of the addition bar for a file.
* Private but used in tests.
*/
- computeBarAdditionX(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
- if (!file || !stats) return;
- return stats.maxAdditionWidth - this.computeBarAdditionWidth(file, stats);
+ computeBarAdditionX(stats: SizeBarLayout) {
+ return stats.additionOffset;
}
/**
@@ -2524,9 +2523,11 @@
/**
* Get the x-offset of the deletion bar for a file.
+ * Private but used in tests.
*/
- private computeBarDeletionX(stats: SizeBarLayout) {
- return stats.deletionOffset;
+ computeBarDeletionX(file?: NormalizedFileInfo, stats?: SizeBarLayout) {
+ if (!file || !stats) return;
+ return stats.maxDeletionWidth - this.computeBarDeletionWidth(file, stats);
}
// Private but used in tests.
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index ab9d038..b2cd430 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -76,7 +76,6 @@
suite('gr-file-list tests', () => {
let element: GrFileList;
-
let saveStub: sinon.SinonStub;
suite('basic tests', async () => {
@@ -106,11 +105,9 @@
element.numFilesShown = 200;
element.basePatchNum = PARENT;
element.patchNum = 2 as RevisionPatchSetNum;
- saveStub = sinon
- .stub(element, '_saveReviewedState')
- .callsFake(() => Promise.resolve());
- await element.updateComplete;
+ saveStub = sinon.stub(element, '_saveReviewedState').resolves();
element.showSizeBars = true;
+ await element.updateComplete;
// Wait for expandedFilesChanged to complete.
await waitEventLoop();
});
@@ -207,14 +204,16 @@
</div>
</div>
<div class="desktop" role="gridcell">
- <div aria-hidden="true" class="sizeBars"></div>
+ <div aria-hidden="true" class="sizeBars">
+ <svg><!-- contents asserted separately below --></svg>
+ </div>
</div>
<div class="stats" role="gridcell">
<div>
- <span aria-label="9 added" class="added" tabindex="0"> +9 </span>
<span aria-label="0 removed" class="removed" tabindex="0">
-0
</span>
+ <span aria-label="9 added" class="added" tabindex="0"> +9 </span>
<span hidden=""> +/-0 B </span>
</div>
</div>
@@ -262,6 +261,31 @@
</div>
</div>`
);
+ // <svg> and contents are ignored by assert.dom.equal() above, so we need
+ // a separate assert.lightDom.equal() for it here.
+ const sizeBarsSVG = queryAndAssert<SVGSVGElement>(
+ element,
+ '.sizeBars > svg'
+ );
+ assert.lightDom.equal(
+ sizeBarsSVG,
+ /* HTML */ `
+ <rect
+ height="8"
+ width="0"
+ x="0"
+ y="0"
+ fill="var(--negative-red-text-color)"
+ ></rect>
+ <rect
+ height="8"
+ width="60"
+ x="1"
+ y="0"
+ fill="var(--positive-green-text-color)"
+ ></rect>
+ `
+ );
});
test('renders file paths', async () => {
@@ -1763,7 +1787,7 @@
maxDeleted: 0,
maxAdditionWidth: 0,
maxDeletionWidth: 0,
- deletionOffset: 0,
+ additionOffset: 0,
};
element.files = [];
@@ -1811,7 +1835,7 @@
maxDeleted: 0,
maxAdditionWidth: 60,
maxDeletionWidth: 0,
- deletionOffset: 60,
+ additionOffset: 60,
};
// Uses half the space when file is half the largest addition and there
@@ -1839,22 +1863,33 @@
assert.equal(element.computeBarAdditionWidth(file, stats), 1.5);
});
- test('_computeBarAdditionX', () => {
+ test('computeBarDeletionX', () => {
const file = {
__path: 'foo/bar.baz',
- lines_inserted: 5,
- lines_deleted: 0,
+ lines_inserted: 0,
+ lines_deleted: 5,
size: 0,
size_delta: 0,
};
const stats = {
+ maxInserted: 0,
+ maxDeleted: 10,
+ maxAdditionWidth: 0,
+ maxDeletionWidth: 60,
+ additionOffset: 60,
+ };
+ assert.equal(element.computeBarDeletionX(file, stats), 30);
+ });
+
+ test('computeBarAdditionX', () => {
+ const stats = {
maxInserted: 10,
maxDeleted: 0,
maxAdditionWidth: 60,
maxDeletionWidth: 0,
- deletionOffset: 60,
+ additionOffset: 60,
};
- assert.equal(element.computeBarAdditionX(file, stats), 30);
+ assert.equal(element.computeBarAdditionX(stats), 60);
});
test('computeBarDeletionWidth', () => {
@@ -1870,7 +1905,7 @@
maxDeleted: 10,
maxAdditionWidth: 30,
maxDeletionWidth: 30,
- deletionOffset: 31,
+ additionOffset: 31,
};
// Uses a quarter the space when file is half the largest deletions and
@@ -1898,7 +1933,7 @@
assert.equal(element.computeBarDeletionWidth(file, stats), 1.5);
});
- test('_computeSizeBarsClass', () => {
+ test('computeSizeBarsClass', () => {
element.showSizeBars = false;
assert.equal(
element.computeSizeBarsClass('foo/bar.baz'),
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 9225ce8..8ae1c36 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -330,6 +330,12 @@
newAttentionSet: Set<UserId> = new Set();
@state()
+ manuallyAddedAttentionSet: Set<UserId> = new Set();
+
+ @state()
+ manuallyDeletedAttentionSet: Set<UserId> = new Set();
+
+ @state()
patchsetLevelDraftIsResolved = true;
@state()
@@ -477,12 +483,14 @@
#pluginMessage:empty {
display: none;
}
- .attention .edit-attention-button {
+ .edit-attention-button {
vertical-align: top;
--gr-button-padding: 0px 4px;
}
- .attention .edit-attention-button gr-icon {
+ .edit-attention-button gr-icon {
color: inherit;
+
+ .attentionSummary .edit-attention-button gr-icon {
/* The line-height:26px hack (see below) requires us to do this.
Normally the gr-icon would account for a proper positioning
within the standard line-height:20px context. */
@@ -999,30 +1007,9 @@
)}
`
)}
- <gr-tooltip-content
- has-tooltip
- title=${this.computeAttentionButtonTitle()}
- >
- <gr-button
- class="edit-attention-button"
- @click=${this.handleAttentionModify}
- ?disabled=${this.isSendDisabled()}
- link
- position-below
- data-label="Edit"
- data-action-type="change"
- data-action-key="edit"
- role="button"
- tabindex="0"
- >
- <div>
- <gr-icon icon="edit" filled small></gr-icon>
- <span>Modify</span>
- </div>
- </gr-button>
- </gr-tooltip-content>
</div>
<div>
+ ${this.renderModifyAttentionSetButton()}
<a
href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
target="_blank"
@@ -1036,6 +1023,30 @@
`;
}
+ private renderModifyAttentionSetButton() {
+ return html` <gr-button
+ class="edit-attention-button"
+ @click=${this.toggleAttentionModify}
+ ?disabled=${this.isSendDisabled()}
+ link
+ position-below
+ data-label="Edit"
+ data-action-type="change"
+ data-action-key="edit"
+ role="button"
+ tabindex="0"
+ >
+ <div>
+ <gr-icon
+ icon=${this.attentionExpanded ? 'expand_circle_up' : 'edit'}
+ filled
+ small
+ ></gr-icon>
+ <span>${this.attentionExpanded ? 'Collapse' : 'Modify'}</span>
+ </div>
+ </gr-button>`;
+ }
+
private renderAttentionDetailsSection() {
if (!this.attentionExpanded) return;
return html`
@@ -1044,8 +1055,10 @@
<div>
<span>Modify attention to</span>
</div>
+
<div></div>
<div>
+ ${this.renderModifyAttentionSetButton()}
<a
href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
target="_blank"
@@ -1137,7 +1150,7 @@
`
)}
${when(
- this.computeShowAttentionTip(),
+ this.computeShowAttentionTip(3),
() => html`
<div class="attentionTip">
<gr-icon icon="lightbulb"></gr-icon>
@@ -1535,8 +1548,8 @@
this.reviewers = getAccounts(ReviewerState.REVIEWER);
}
- handleAttentionModify() {
- this.attentionExpanded = true;
+ toggleAttentionModify() {
+ this.attentionExpanded = !this.attentionExpanded;
}
onAttentionExpandedChange() {
@@ -1545,13 +1558,6 @@
fire(this, 'iron-resize', {});
}
- computeAttentionButtonTitle() {
- return this.isSendDisabled()
- ? 'Modify the attention set by adding a comment or use the account ' +
- 'hovercard in the change page.'
- : 'Edit attention set changes';
- }
-
handleAttentionClick(e: Event) {
const targetAccount = (e.target as GrAccountChip)?.account;
if (!targetAccount) return;
@@ -1563,11 +1569,15 @@
if (this.newAttentionSet.has(id)) {
this.newAttentionSet.delete(id);
+ this.manuallyAddedAttentionSet.delete(id);
+ this.manuallyDeletedAttentionSet.add(id);
this.reporting.reportInteraction(Interaction.ATTENTION_SET_CHIP, {
action: `REMOVE${self}${role}`,
});
} else {
this.newAttentionSet.add(id);
+ this.manuallyAddedAttentionSet.add(id);
+ this.manuallyDeletedAttentionSet.delete(id);
this.reporting.reportInteraction(Interaction.ATTENTION_SET_CHIP, {
action: `ADD${self}${role}`,
});
@@ -1657,18 +1667,24 @@
.map(a => getUserId(a))
.filter(id => !!id);
this.newAttentionSet = new Set([
- ...[...newAttention].filter(id => allAccountIds.includes(id)),
+ ...this.manuallyAddedAttentionSet,
+ ...[...newAttention].filter(
+ id =>
+ allAccountIds.includes(id) &&
+ !this.manuallyDeletedAttentionSet.has(id)
+ ),
]);
-
- this.attentionExpanded = this.computeShowAttentionTip();
+ // Possibly expand if need be, never collapse as this is jarring to the user.
+ this.attentionExpanded =
+ this.attentionExpanded || this.computeShowAttentionTip(1);
}
- computeShowAttentionTip() {
+ computeShowAttentionTip(minimum: number) {
if (!this.currentAttentionSet || !this.newAttentionSet) return false;
const addedIds = [...this.newAttentionSet].filter(
id => !this.currentAttentionSet.has(id)
);
- return this.isOwner && addedIds.length > 2;
+ return this.isOwner && addedIds.length >= minimum;
}
computeCommentAccounts(threads: CommentThread[]) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 3c3c246..5766d80 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -255,30 +255,25 @@
<div class="attentionSummary">
<div>
<span> No changes to the attention set. </span>
- <gr-tooltip-content
- has-tooltip=""
- title="Modify the attention set by adding a comment or use the account hovercard in the change page."
- >
- <gr-button
- aria-disabled="true"
- disabled=""
- class="edit-attention-button"
- data-action-key="edit"
- data-action-type="change"
- data-label="Edit"
- link=""
- position-below=""
- role="button"
- tabindex="-1"
- >
- <div>
- <gr-icon icon="edit" filled small></gr-icon>
- <span>Modify</span>
- </div>
- </gr-button>
- </gr-tooltip-content>
</div>
<div>
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ class="edit-attention-button"
+ data-action-key="edit"
+ data-action-type="change"
+ data-label="Edit"
+ link=""
+ position-below=""
+ role="button"
+ tabindex="-1"
+ >
+ <div>
+ <gr-icon icon="edit" filled small></gr-icon>
+ <span>Modify</span>
+ </div>
+ </gr-button>
<a
href="https://gerrit-review.googlesource.com/Documentation/user-attention-set.html"
target="_blank"
@@ -1732,19 +1727,17 @@
element._ccs = [...element.ccs, makeAccount()];
await element.updateComplete;
- // The 'attention modified' section collapses and resets when reviewers or
- // ccs change.
- assert.isFalse(element.attentionExpanded);
-
- queryAndAssert<GrButton>(element, '.edit-attention-button').click();
- await element.updateComplete;
-
assert.isTrue(element.attentionExpanded);
accountLabels = Array.from(
queryAll(element, '.attention-detail gr-account-label')
);
assert.equal(accountLabels.length, 7);
+ // Verify that toggling the attention-set-button collapses.
+ queryAndAssert<GrButton>(element, '.edit-attention-button').click();
+ await element.updateComplete;
+ assert.isFalse(element.attentionExpanded);
+
element.reviewers.pop();
element.reviewers.pop();
element._ccs.pop();
@@ -2556,6 +2549,59 @@
});
});
+ test('manually added users are not lost when view updates.', async () => {
+ assert.sameMembers([...element.newAttentionSet], []);
+
+ element.reviewers = [
+ createAccountWithId(1),
+ createAccountWithId(2),
+ createAccountWithId(3),
+ ];
+ element.patchsetLevelDraftMessage = 'abc';
+
+ await element.updateComplete;
+ assert.sameMembers(
+ [...element.newAttentionSet],
+ [2 as AccountId, 3 as AccountId, 999 as AccountId]
+ );
+
+ const modifyButton = queryAndAssert<GrButton>(
+ element,
+ '.edit-attention-button'
+ );
+
+ modifyButton.click();
+ assert.isTrue(element.attentionExpanded);
+ await element.updateComplete;
+
+ const accountsChips = Array.from(
+ queryAll<GrAccountLabel>(element, '.attention-detail gr-account-label')
+ );
+ assert.equal(accountsChips.length, 4);
+ for (let i = 0; i < 4; ++i) {
+ if (accountsChips[i].account?._account_id === 1) {
+ accountsChips[i].click();
+ break;
+ }
+ }
+
+ await element.updateComplete;
+
+ assert.sameMembers(
+ [...element.newAttentionSet],
+ [1 as AccountId, 2 as AccountId, 3 as AccountId, 999 as AccountId]
+ );
+
+ // Doesn't get reset when message changes.
+ element.patchsetLevelDraftMessage = 'def';
+ await element.updateComplete;
+
+ assert.sameMembers(
+ [...element.newAttentionSet],
+ [1 as AccountId, 2 as AccountId, 3 as AccountId, 999 as AccountId]
+ );
+ });
+
suite('mention users', () => {
setup(async () => {
element.account = createAccountWithId(1);
@@ -2692,6 +2738,13 @@
await element.updateComplete;
assert.sameMembers([...element.newAttentionSet], [999 as AccountId]);
+
+ // Random update
+ element.patchsetLevelDraftMessage = 'abc';
+ await element.updateComplete;
+
+ assert.sameMembers([...element.newAttentionSet], [999 as AccountId]);
+ element.patchsetLevelDraftMessage = 'abc';
});
test('mention user who is already CCed', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
index b6af123..3dcba9e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.ts
@@ -187,8 +187,11 @@
}
protected override willUpdate(changedProperties: PropertyValues): void {
+ if (changedProperties.has('value') || changedProperties.has('items')) {
+ this.updateText();
+ }
if (changedProperties.has('value')) {
- this.handleValueChange();
+ fireNoBubble(this, 'value-change', {value: this.value});
}
}
@@ -307,7 +310,7 @@
}, 1);
}
- private handleValueChange() {
+ private updateText() {
if (this.value === undefined || this.items === undefined) {
return;
}
@@ -318,7 +321,6 @@
this.text = selectedObj.triggerText
? selectedObj.triggerText
: selectedObj.text;
- fireNoBubble(this, 'value-change', {value: this.value});
}
/**
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
index 70e653a..e557ca8 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.ts
@@ -147,20 +147,17 @@
test('move action button to overflow', async () => {
const key = changeActions.add(ActionType.REVISION, 'Bork!');
await element.updateComplete;
- assert.isTrue(queryAndAssert<GrDropdown>(element, '#moreActions').hidden);
- assert.isOk(
- queryAndAssert<GrButton>(element, `[data-action-key="${key}"]`)
- );
+
+ let items = queryAndAssert<GrDropdown>(element, '#moreActions').items;
+ assert.isFalse(items?.some(item => item.name === 'Bork!'));
+ assert.isOk(query<GrButton>(element, `[data-action-key="${key}"]`));
+
changeActions.setActionOverflow(ActionType.REVISION, key, true);
await element.updateComplete;
+
+ items = queryAndAssert<GrDropdown>(element, '#moreActions').items;
+ assert.isTrue(items?.some(item => item.name === 'Bork!'));
assert.isNotOk(query<GrButton>(element, `[data-action-key="${key}"]`));
- assert.isFalse(
- queryAndAssert<GrDropdown>(element, '#moreActions').hidden
- );
- assert.strictEqual(
- queryAndAssert<GrDropdown>(element, '#moreActions').items![0].name,
- 'Bork!'
- );
});
test('change actions priority', async () => {