Merge "user-review-ui: make colon usage a little more consistent"
diff --git a/.gitignore b/.gitignore
index 53bc9f6..0bbcaba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,8 @@
/infer-out
/local.properties
/node_modules/
+/polygerrit-ui/node_modules/
+/polygerrit-ui/app/node_modules/
/package-lock.json
/plugins/*
/polygerrit-ui/coverage/
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 3da69df..185fa07 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -800,6 +800,22 @@
Users without this access right who are able to upload changes can
still do the revert locally and upload the revert commit as a new change.
+[[category_remove_label]]
+=== Remove Label (Remove Vote)
+
+For every configured label `My-Name` in the project, there is a
+corresponding permission `removeLabel-My-Name` with a range corresponding to
+the defined values. For these values, the users are permitted to remove
+other users' votes from a change.
+
+Change owners can always remove zero or positive votes (even without
+having the `Remove Vote` access right assigned).
+
+Project owners and site administrators can always remove any vote (even
+without having the `Remove Vote` access right assigned).
+
+Users without this access right can still remove their own votes.
+
[[category_remove_reviewer]]
=== Remove Reviewer
@@ -890,6 +906,9 @@
the Work In Progress bit of the change (even without having the
`Toggle Work In Progress state` access right assigned).
+Must be assigned on the target branch ref (i.e. on 'refs/heads/*', not on
+'refs/for/*').
+
[[category_delete_own_changes]]
=== Delete Own Changes
@@ -1351,10 +1370,11 @@
[[capability_createProject]]
=== Create Project
-Allow project creation. This capability allows the granted group to
-either link:cmd-create-project.html[create new git projects via ssh]
-or via the web UI.
+Allow project creation.
+This capability allows the granted group to create projects via the web UI, via
+link:rest-api-projects.html#create-project][REST] and via
+link:cmd-create-project.html[SSH].
[[capability_emailReviewers]]
=== Email Reviewers
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 601f2bf..fb37287 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -282,6 +282,22 @@
canOverrideInChildProjects = true
----
+Branch configuration supports regular expressions as well, e.g. to exempt 'refs/heads/release/*' pattern,
+when migrating from the label Submit-Rule:
+
+----
+[label "Verified"]
+ branch = refs/heads/release/*
+----
+
+The following SR can be configured:
+
+----
+[submit-requirement "Verified"]
+ submittableIf = label:Verified=MAX AND -label:Verified=MIN
+ applicableIf = branch:^refs/heads/release/.*
+----
+
[[test-submit-requirements]]
== Testing Submit Requirements
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 264ce73..7a042ba 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -1014,7 +1014,7 @@
inline comment ("Yeah, I see why, let me try again.").
[[security-fixes]]
--- Security Fixes
+== Security Fixes
If a security vulnerability is discovered you normally want to have an
embargo about it until fixed releases have been made available. This
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 3f7d90d..d7343c2 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -98,20 +98,29 @@
See link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/styles/themes/[app-theme.ts]
for the list of available variables.
-Just add code like this to your JavaScript plugin:
+You can just create `<style>` elements yourself and add them to the
+`document.head`, but for your convenience the Plugin API provides a simple
+`styleApi().insertCSSRule()` method for doing just that. Typically you would
+define a CSS rule for `html`, which is always applied, or for a specific theme
+such as `html.lightTheme`.
``` js
Gerrit.install(plugin => {
- const styleEl = document.createElement('style');
- styleEl.innerHTML = `
- html {
- --header-background-color: #c3d9ff;
- }
- html.darkTheme {
- --header-background-color: #c3d9ff90;
- }
- `;
- document.head.appendChild(styleEl);
+ plugin.styleApi().insertCSSRule(`
+ html {
+ --header-text-color: black;
+ }
+ `);
+ plugin.styleApi().insertCSSRule(`
+ html.lightTheme {
+ --header-background-color: red;
+ }
+ `);
+ plugin.styleApi().insertCSSRule(`
+ html.darkTheme {
+ --header-background-color: blue;
+ }
+ `);
});
```
@@ -154,12 +163,6 @@
See link:pg-plugin-endpoints.html[endpoints].
-=== registerStyleModule
-`plugin.registerStyleModule(endpointName, moduleName)`
-
-This API is deprecated and will be removed either in version 3.6 or 3.7,
-see link:#low-level-style[above] for an alternative.
-
=== on
Register a JavaScript callback to be invoked when events occur within
the web interface. Signature
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index 41f544d..dd82f27 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -132,6 +132,10 @@
=== settings-screen
This endpoint is situated at the end of the body of the settings screen.
+=== profile
+This endpoint is situated at the top of the Profile section of the settings
+screen below the section description text.
+
=== reply-text
This endpoint wraps the textarea in the reply dialog.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 88018a6..2f144c6 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -103,15 +103,17 @@
"id": "demo~master~Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
"project": "demo",
"branch": "master",
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
- "name": "John Doe"
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
},
- "last_update": "2012-07-17 07:19:27.766000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2012-07-17 07:19:27.766000000",
+ "reason": "reviewer or cc replied"
}
- ]
+ },
"change_id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
"subject": "One change",
"status": "NEW",
@@ -545,15 +547,17 @@
"id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
"project": "myProject",
"branch": "master",
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
- "name": "John Doe"
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
},
- "last_update": "2013-02-21 11:16:36.775000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
}
- ]
+ },
"change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
"subject": "Implementing Feature X",
"status": "NEW",
@@ -612,15 +616,17 @@
)]}'
{
"added": {
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
- "name": "John Doe"
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
},
- "last_update": "2013-02-21 11:16:36.775000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
}
- ]
+ },
"updated": "2013-02-21 11:16:36.775000000",
"topic": "new-topic"
},
@@ -651,15 +657,17 @@
"id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
"project": "myProject",
"branch": "master",
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
- "name": "John Doe"
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
},
- "last_update": "2013-02-21 11:16:36.775000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
}
- ],
+ },
"change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
"subject": "Implementing Feature X",
"status": "NEW",
@@ -719,18 +727,18 @@
"id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
"project": "myProject",
"branch": "master",
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
"_account_id": 1000096,
"name": "John Doe",
"email": "john.doe@example.com",
"username": "jdoe"
},
- "last_update": "2013-02-21 11:16:36.775000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
}
- ]
+ },
"change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
"subject": "Implementing Feature X",
"status": "NEW",
@@ -816,6 +824,26 @@
"+2"
]
},
+ "removable_labels": {
+ "Code-Review": {
+ "-1": [
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "jdoe"
+ }
+ ],
+ "+1": [
+ {
+ "_account_id": 1000097,
+ "name": "Jane Roe",
+ "email": "jane.roe@example.com",
+ "username": "jroe"
+ }
+ ]
+ }
+ },
"removable_reviewers": [
{
"_account_id": 1000096,
@@ -1505,6 +1533,223 @@
The change could not be rebased due to a path conflict during merge.
----
+[[rebase-chain]]
+=== Rebase Chain
+--
+'POST /changes/link:#change-id[\{change-id\}]/rebase:chain'
+--
+
+Rebases an ancestry chain of changes.
+
+The operated change is treated as the chain tip. All unsubmitted ancestors are rebased.
+
+Requires a linear ancestry relation (single parenting throughout the chain).
+
+Optionally, the parent revision (of the oldest ancestor to be rebased) can be changed to another
+change, revision or branch through the link:#rebase-input[RebaseInput] entity.
+
+If the chain is outdated, i.e., there's a change that depends on an old revision of its parent, the
+result is the same as individually rebasing all outdated changes on top of their parent's latest
+revision before running the rebase chain action.
+
+.Request
+----
+ POST /changes/myProject~master~I08a021fb07b83fe845140a2c11508b3bdd93b48f/rebase:chain HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "base" : "1234",
+ }
+----
+
+As response a link:#rebase-chain-info[RebaseChainInfo] entity is returned that
+describes the rebased changes. Information about the current patch sets
+are included.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "rebased_changes": [
+ {
+ "id": "myProject~master~I0e534de9d7f0d6f35b71f7d726acf835b2110c66",
+ "project": "myProject",
+ "branch": "master",
+ "hashtags": [
+
+ ],
+ "change_id": "I0e534de9d7f0d6f35b71f7d726acf835b2110c66",
+ "subject": "456",
+ "status": "NEW",
+ "created": "2022-11-21 20: 51: 31.000000000",
+ "updated": "2022-11-21 20: 56: 49.000000000",
+ "submit_type": "MERGE_IF_NECESSARY",
+ "insertions": 0,
+ "deletions": 0,
+ "total_comment_count": 0,
+ "unresolved_comment_count": 0,
+ "has_review_started": true,
+ "meta_rev_id": "a2a6692213f546e1045ecf4647439fac8d6d8faa",
+ "_number": 21,
+ "owner": {
+ "_account_id": 1000000
+ },
+ "current_revision": "c3b2ba222d42a56e05c90f88d4509a124620517d",
+ "revisions": {
+ "c3b2ba222d42a56e05c90f88d4509a124620517d": {
+ "kind": "NO_CHANGE",
+ "_number": 2,
+ "created": "2022-11-21 20: 56: 49.000000000",
+ "uploader": {
+ "_account_id": 1000000
+ },
+ "ref": "refs/changes/21/21/2",
+ "fetch": {
+
+ },
+ "commit": {
+ "parents": [
+ {
+ "commit": "7803f427dd7c4a2441466e4d740a1850dcee1af4",
+ "subject": "123"
+ }
+ ],
+ "author": {
+ "name": "Nitzan Gur-Furman",
+ "email": "nitzan@google.com",
+ "date": "2022-11-21 20: 49: 39.000000000",
+ "tz": 60
+ },
+ "committer": {
+ "name": "Administrator",
+ "email": "admin@example.com",
+ "date": "2022-11-21 20: 56: 49.000000000",
+ "tz": 60
+ },
+ "subject": "456",
+ "message": "456\n"
+ },
+ "description": "Rebase"
+ }
+ },
+ "requirements": [
+
+ ],
+ "submit_records": [
+ {
+ "rule_name": "gerrit~DefaultSubmitRule",
+ "status": "NOT_READY",
+ "labels": [
+ {
+ "label": "Code-Review",
+ "status": "NEED"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "myProject~master~I08a021fb07b83fe845140a2c11508b3bdd93b48f",
+ "project": "myProject",
+ "branch": "master",
+ "hashtags": [
+
+ ],
+ "change_id": "I08a021fb07b83fe845140a2c11508b3bdd93b48f",
+ "subject": "789",
+ "status": "NEW",
+ "created": "2022-11-21 20: 51: 31.000000000",
+ "updated": "2022-11-21 20: 56: 49.000000000",
+ "submit_type": "MERGE_IF_NECESSARY",
+ "insertions": 0,
+ "deletions": 0,
+ "total_comment_count": 0,
+ "unresolved_comment_count": 0,
+ "has_review_started": true,
+ "meta_rev_id": "3bfb843fea471f96e16b9199c3a30fff0285bc45",
+ "_number": 22,
+ "owner": {
+ "_account_id": 1000000
+ },
+ "current_revision": "77eb17a9501a5c21963bc6af56085e60f281acbb",
+ "revisions": {
+ "77eb17a9501a5c21963bc6af56085e60f281acbb": {
+ "kind": "NO_CHANGE",
+ "_number": 2,
+ "created": "2022-11-21 20: 56: 49.000000000",
+ "uploader": {
+ "_account_id": 1000000
+ },
+ "ref": "refs/changes/22/22/2",
+ "fetch": {
+
+ },
+ "commit": {
+ "parents": [
+ {
+ "commit": "c3b2ba222d42a56e05c90f88d4509a124620517d",
+ "subject": "456"
+ }
+ ],
+ "author": {
+ "name": "Nitzan Gur-Furman",
+ "email": "nitzan@google.com",
+ "date": "2022-11-21 20: 51: 07.000000000",
+ "tz": 60
+ },
+ "committer": {
+ "name": "Administrator",
+ "email": "admin@example.com",
+ "date": "2022-11-21 20: 56: 49.000000000",
+ "tz": 60
+ },
+ "subject": "789",
+ "message": "789\n"
+ },
+ "description": "Rebase"
+ }
+ },
+ "requirements": [
+
+ ],
+ "submit_records": [
+ {
+ "rule_name": "gerrit~DefaultSubmitRule",
+ "status": "NOT_READY",
+ "labels": [
+ {
+ "label": "Code-Review",
+ "status": "NEED"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ }
+----
+
+If the change cannot be rebased, e.g. due to conflicts, the response is
+"`409 Conflict`" and the error message is contained in the response
+body.
+
+.Response
+----
+ HTTP/1.1 409 Conflict
+ Content-Disposition: attachment
+ Content-Type: text/plain; charset=UTF-8
+
+ Change I0e534de9d7f0d6f35b71f7d726acf835b2110c66 could not be rebased due to a conflict during
+ merge.
+
+ merge conflict(s):
+ a.txt
+----
+
[[move-change]]
=== Move Change
--
@@ -6520,7 +6765,7 @@
* a commit ID ("674ac754f91e64a0efb8087e59a176484bd534d1")
* an abbreviated commit ID that uniquely identifies one revision of the
change ("674ac754"), at least 4 digits are required
-* a legacy numeric patch number ("1" for first patch set of the change)
+* a numeric patch number ("1" for first patch set of the change)
* "0" or the literal `edit` for a change edit
[[json-entities]]
@@ -6846,6 +7091,13 @@
A map of the permitted labels that maps a label name to the list of
values that are allowed for that label. +
Only set if link:#detailed-labels[detailed labels] are requested.
+|`removable_labels` |optional|
+A map of the removable labels that maps a label name to the map of
+values and reviewers (
+link:rest-api-accounts.html#account-info[AccountInfo] entities)
+that are allowed to be removed from the change. +
+Only set if link:#labels[labels] or
+link:#detailed-labels[detailed labels] are requested.
|`removable_reviewers`|optional|
The reviewers that can be removed by the calling user as a list of
link:rest-api-accounts.html#account-info[AccountInfo] entities. +
@@ -6964,11 +7216,16 @@
Whether the new change should be set to work in progress.
|`base_change` |optional|
A link:#change-id[\{change-id\}] that identifies the base change for a create
-change operation. Mutually exclusive with `base_commit`.
+change operation. +
+Mutually exclusive with `base_commit`. +
+If neither `base_commit` nor `base_change` are set, the target branch tip will
+be used as the parent commit.
|`base_commit` |optional|
A 40-digit hex SHA-1 of the commit which will be the parent commit of the newly
-created change. If set, it must be a merged commit on the destination branch.
-Mutually exclusive with `base_change`.
+created change. If set, it must be a merged commit on the destination branch. +
+Mutually exclusive with `base_change`. +
+If neither `base_commit` nor `base_change` are set, the target branch tip will
+be used as the parent commit.
|`new_branch` |optional, default to `false`|
Allow creating a new branch when set to `true`. Using this option is
only possible for non-merge commits (if the `merge` field is not set).
@@ -7987,6 +8244,22 @@
options. Unknown validation options are silently ignored.
|===========================
+[[rebase-chain-info]]
+=== RebaseChainInfo
+
+The `RebaseChainInfo` entity contains information about a chain of changes
+that were rebased.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`rebased_changes` ||List of the unsubmitted ancestors, as link:#change-info[ChangeInfo]
+entities. Includes both rebased changes, and previously up-to-date ancestors. The list is ordered by
+ancestry, where the oldest ancestor is the first.
+|`contains_git_conflicts` ||Whether any of the rebased changes has conflicts
+due to rebasing.
+|===========================
+
[[related-change-and-commit-info]]
=== RelatedChangeAndCommitInfo
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 9527a56..e12c27c 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -43,6 +43,17 @@
For more predictable results, use explicit search operators as described
in the following section.
+[IMPORTANT]
+--
+The change search API is backed by a secondary index and might sometimes return
+stale results if the re-indexing operation failed for a change update.
+
+Please also note that changes are not re-indexed if the project configuration
+is updated with newly added or modified
+link:config-submit-requirements.html[submit requirements].
+--
+
+
[[search-operators]]
== Search Operators
@@ -347,6 +358,18 @@
regular expressions is limited to the first 32766 bytes of the
commit message due to limitations in Lucene.
+[[subject]]
+subject:'SUBJECT'::
++
+Changes that have a commit message where the first line (aka the subject)
+matches 'SUBJECT'. The matching is done by full text search over the subject.
+
+[[prefixsubject]]
+prefixsubject:'PREFIX'::
++
+Changes that have a commit message where the first line (aka the subject)
+has the prefix 'PREFIX'.
+
[[comment]]
comment:'TEXT'::
+
diff --git a/contrib/git-gc-preserve b/contrib/git-gc-preserve
old mode 100644
new mode 100755
index 54b8fca..a886721
--- a/contrib/git-gc-preserve
+++ b/contrib/git-gc-preserve
@@ -13,25 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-usage() { # error_message
+usage() { # exit code
cat <<-EOF
NAME
git-gc-preserve - Run git gc and preserve old packs to avoid races for JGit
- This command uses custom git config options to configure if preserved packs
- from the last run of git gc should be pruned and if packs should be preserved.
-
- This is similar to the implementation in JGit [1] which is used by
- JGit to avoid errors [2] in such situations.
-
- Don't run multiple instances of this command concurrently on the same
- repository since it does not attempt to implement the file locking
- which git gc --auto does [3].
-
- [1] https://git.eclipse.org/r/c/jgit/jgit/+/87969
- [2] https://git.eclipse.org/r/c/jgit/jgit/+/122288
- [3] https://github.com/git/git/commit/64a99eb4760de2ce2f0c04e146c0a55c34f50f20
-
SYNOPSIS
git gc-preserve
@@ -39,6 +25,29 @@
Runs git gc and can preserve old packs to avoid races with concurrently
executed commands in JGit.
+ This command uses custom git config options to configure if preserved packs
+ from the last run of git gc should be pruned and if packs should be preserved.
+
+ This is similar to the implementation in JGit [1] which is used by
+ JGit to avoid errors [2] in such situations.
+
+ The command prevents concurrent runs of the command on the same repository
+ by acquiring an exclusive file lock on the file
+ "\$repopath/gc-preserve.pid"
+ If it cannot acquire the lock it fails immediately with exit code 3.
+
+ Failure Exit Codes
+ 1: General failure
+ 2: Couldn't determine repository path. If the current working directory
+ is outside of the working tree of the git repository use git option
+ --git-dir to pass the root path of the repository.
+ E.g.
+ $ git --git-dir ~/git/foo gc-preserve
+ 3: Another process already runs $0 on the same repository
+
+ [1] https://git.eclipse.org/r/c/jgit/jgit/+/87969
+ [2] https://git.eclipse.org/r/c/jgit/jgit/+/122288
+
CONFIGURATION
"gc.prunepreserved": if set to "true" preserved packs from the last gc run
are pruned before current packs are preserved.
@@ -49,7 +58,30 @@
across missing objects which might be caused by a concurrent run of
git gc.
EOF
- exit
+ exit "$1"
+}
+
+# acquire file lock, unlock when the script exits
+lock() { # repo
+ readonly LOCKFILE="$1/gc-preserve.pid"
+ test -f "$LOCKFILE" || touch "$LOCKFILE"
+ exec 9> "$LOCKFILE"
+ if flock -nx 9; then
+ echo -n "$$ $USERNAME@$HOSTNAME" >&9
+ trap unlock EXIT
+ else
+ echo "$0 is already running"
+ exit 3
+ fi
+}
+
+unlock() {
+ # only delete if the file descriptor 9 is open
+ if { : >&9 ; } &> /dev/null; then
+ rm -f "$LOCKFILE"
+ fi
+ # close the file handle to release file lock
+ exec 9>&-
}
# prune preserved packs if gc.prunepreserved == true
@@ -74,7 +106,7 @@
return 0
fi
local packdir=$1/objects/pack
- pushd "$packdir" >/dev/null || exit
+ pushd "$packdir" >/dev/null || exit 1
mkdir -p preserved
printf "Preserving packs: "
count=0
@@ -85,7 +117,7 @@
fi
done
echo "$count, done."
- popd >/dev/null || exit
+ popd >/dev/null || exit 1
}
# pack-0...2.pack to pack-0...2.old-pack
@@ -100,7 +132,7 @@
while [ $# -gt 0 ] ; do
case "$1" in
- -u|-h) usage ;;
+ -u|-h) usage 0 ;;
esac
shift
done
@@ -108,10 +140,10 @@
repopath=$(git rev-parse --git-dir)
if [ -z "$repopath" ]; then
- usage
- exit $?
+ usage 2
fi
+lock "$repopath"
prune_preserved "$repopath"
preserve_packs "$repopath"
-git gc ${args:+"$args"}
+git gc ${args:+"$args"} || { echo "git gc failed"; exit "$?"; }
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 5b4a9e5..ccf74d1 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1393,6 +1393,17 @@
}
}
+ protected void assertOnlyRemovableLabel(
+ ChangeInfo info, String labelId, String labelValue, TestAccount reviewer) {
+ assertThat(info.removableLabels).hasSize(1);
+ assertThat(info.removableLabels).containsKey(labelId);
+ assertThat(info.removableLabels.get(labelId)).hasSize(1);
+ assertThat(info.removableLabels.get(labelId)).containsKey(labelValue);
+ assertThat(info.removableLabels.get(labelId).get(labelValue)).hasSize(1);
+ assertThat(info.removableLabels.get(labelId).get(labelValue).get(0).email)
+ .isEqualTo(reviewer.email());
+ }
+
protected void assertPermissions(
Project.NameKey project,
GroupReference groupReference,
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index c67991d..ff5bc00 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -141,6 +141,10 @@
return create(username, null, username, null, (String[]) null);
}
+ public TestAccount createValid(String username) throws Exception {
+ return create(username, username + "@example.com", username, username);
+ }
+
public TestAccount admin() throws Exception {
return create("admin", "admin@example.com", "Administrator", "Adminny", "Administrators");
}
diff --git a/java/com/google/gerrit/acceptance/DisabledAccountIndex.java b/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
index 7bd0c73..7660948 100644
--- a/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
@@ -55,6 +55,11 @@
}
@Override
+ public void deleteByValue(AccountState value) {
+ throw new UnsupportedOperationException("AccountIndex is disabled");
+ }
+
+ @Override
public void delete(Account.Id key) {
throw new UnsupportedOperationException("AccountIndex is disabled");
}
diff --git a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
index 7671ad4..c028a8e 100644
--- a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
@@ -62,6 +62,11 @@
}
@Override
+ public void deleteByValue(ChangeData value) {
+ throw new UnsupportedOperationException("ChangeIndex is disabled");
+ }
+
+ @Override
public void delete(Change.Id key) {
throw new UnsupportedOperationException("ChangeIndex is disabled");
}
diff --git a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
index 2e3dd90..f2aad4a 100644
--- a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
@@ -60,6 +60,11 @@
}
@Override
+ public void deleteByValue(ProjectData value) {
+ throw new UnsupportedOperationException("ProjectIndex is disabled");
+ }
+
+ @Override
public void delete(Project.NameKey key) {
throw new UnsupportedOperationException("ProjectIndex is disabled");
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index b1cd506..69139ce 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -197,8 +197,13 @@
PermissionRule.Builder rule = newRule(projectConfig, p.group());
rule.setAction(p.action());
rule.setRange(p.min(), p.max());
- String permissionName =
- p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(p.name());
+ String permissionName;
+ if (p.isAddPermission()) {
+ permissionName =
+ p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(p.name());
+ } else {
+ permissionName = Permission.forRemoveLabel(p.name());
+ }
projectConfig.upsertAccessSection(
p.ref(), as -> as.upsertPermission(permissionName).add(rule));
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
index 9a9a21a..5634c78 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -162,12 +162,34 @@
/** Starts a builder for allowing a label permission. */
public static TestLabelPermission.Builder allowLabel(String name) {
- return TestLabelPermission.builder().name(name).action(PermissionRule.Action.ALLOW);
+ return TestLabelPermission.builder()
+ .name(name)
+ .isAddPermission(true)
+ .action(PermissionRule.Action.ALLOW);
}
/** Starts a builder for denying a label permission. */
public static TestLabelPermission.Builder blockLabel(String name) {
- return TestLabelPermission.builder().name(name).action(PermissionRule.Action.BLOCK);
+ return TestLabelPermission.builder()
+ .name(name)
+ .isAddPermission(true)
+ .action(PermissionRule.Action.BLOCK);
+ }
+
+ /** Starts a builder for allowing a remove-label permission. */
+ public static TestLabelPermission.Builder allowLabelRemoval(String name) {
+ return TestLabelPermission.builder()
+ .name(name)
+ .isAddPermission(false)
+ .action(PermissionRule.Action.ALLOW);
+ }
+
+ /** Starts a builder for denying a remove-label permission. */
+ public static TestLabelPermission.Builder blockLabelRemoval(String name) {
+ return TestLabelPermission.builder()
+ .name(name)
+ .isAddPermission(false)
+ .action(PermissionRule.Action.BLOCK);
}
/** Records a label permission to be updated. */
@@ -191,6 +213,8 @@
abstract boolean impersonation();
+ abstract boolean isAddPermission();
+
/** Builder for {@link TestLabelPermission}. */
@AutoValue.Builder
public abstract static class Builder {
@@ -208,6 +232,8 @@
abstract Builder max(int max);
+ abstract Builder isAddPermission(boolean isAddPermission);
+
/** Sets the minimum and maximum values for the permission. */
public Builder range(int min, int max) {
checkArgument(min != 0 || max != 0, "empty range");
@@ -243,6 +269,12 @@
return TestPermissionKey.builder().name(Permission.forLabel(name));
}
+ /** Starts a builder for describing a label removal permission key for deletion. */
+ public static TestPermissionKey.Builder labelRemovalPermissionKey(String name) {
+ checkLabelName(name);
+ return TestPermissionKey.builder().name(Permission.forRemoveLabel(name));
+ }
+
/** Starts a builder for describing a capability key for deletion. */
public static TestPermissionKey.Builder capabilityKey(String name) {
return TestPermissionKey.builder().name(name).section(GLOBAL_CAPABILITIES);
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 6d2fa32..d029fad 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -43,6 +43,7 @@
public static final String FORGE_SERVER = "forgeServerAsCommitter";
public static final String LABEL = "label-";
public static final String LABEL_AS = "labelAs-";
+ public static final String REMOVE_LABEL = "removeLabel-";
public static final String OWNER = "owner";
public static final String PUSH = "push";
public static final String PUSH_MERGE = "pushMerge";
@@ -60,6 +61,7 @@
private static final List<String> NAMES_LC;
private static final int LABEL_INDEX;
private static final int LABEL_AS_INDEX;
+ private static final int REMOVE_LABEL_INDEX;
static {
NAMES_LC = new ArrayList<>();
@@ -79,6 +81,7 @@
NAMES_LC.add(FORGE_SERVER.toLowerCase());
NAMES_LC.add(LABEL.toLowerCase());
NAMES_LC.add(LABEL_AS.toLowerCase());
+ NAMES_LC.add(REMOVE_LABEL.toLowerCase());
NAMES_LC.add(OWNER.toLowerCase());
NAMES_LC.add(PUSH.toLowerCase());
NAMES_LC.add(PUSH_MERGE.toLowerCase());
@@ -93,15 +96,19 @@
LABEL_INDEX = NAMES_LC.indexOf(Permission.LABEL);
LABEL_AS_INDEX = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
+ REMOVE_LABEL_INDEX = NAMES_LC.indexOf(Permission.REMOVE_LABEL.toLowerCase());
}
/** Returns true if the name is recognized as a permission name. */
public static boolean isPermission(String varName) {
- return isLabel(varName) || isLabelAs(varName) || NAMES_LC.contains(varName.toLowerCase());
+ return isLabel(varName)
+ || isLabelAs(varName)
+ || isRemoveLabel(varName)
+ || NAMES_LC.contains(varName.toLowerCase());
}
public static boolean hasRange(String varName) {
- return isLabel(varName) || isLabelAs(varName);
+ return isLabel(varName) || isLabelAs(varName) || isRemoveLabel(varName);
}
/** Returns true if the permission name is actually for a review label. */
@@ -114,6 +121,11 @@
return var.startsWith(LABEL_AS) && LABEL_AS.length() < var.length();
}
+ /** Returns true if the permission is for impersonated review labels. */
+ public static boolean isRemoveLabel(String var) {
+ return var.startsWith(REMOVE_LABEL) && REMOVE_LABEL.length() < var.length();
+ }
+
/** Returns permission name for the given review label. */
public static String forLabel(String labelName) {
return LABEL + labelName;
@@ -124,12 +136,19 @@
return LABEL_AS + labelName;
}
+ /** Returns permission name to remove a label for another user. */
+ public static String forRemoveLabel(String labelName) {
+ return REMOVE_LABEL + labelName;
+ }
+
@Nullable
public static String extractLabel(String varName) {
if (isLabel(varName)) {
return varName.substring(LABEL.length());
} else if (isLabelAs(varName)) {
return varName.substring(LABEL_AS.length());
+ } else if (isRemoveLabel(varName)) {
+ return varName.substring(REMOVE_LABEL.length());
}
return null;
}
@@ -205,6 +224,8 @@
return LABEL_INDEX;
} else if (isLabelAs(a.getName())) {
return LABEL_AS_INDEX;
+ } else if (isRemoveLabel(a.getName())) {
+ return REMOVE_LABEL_INDEX;
}
int index = NAMES_LC.indexOf(a.getName().toLowerCase());
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index cce28e9..0ebb859 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -27,12 +27,14 @@
import com.google.gerrit.extensions.common.CommitMessageInput;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.common.RebaseChainInfo;
import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInput;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Arrays;
import java.util.Collection;
@@ -178,6 +180,24 @@
/** Rebase the current revision of a change. */
void rebase(RebaseInput in) throws RestApiException;
+ /**
+ * Rebase the current revisions of a change's chain using default options.
+ *
+ * @return a {@code RebaseChainInfo} contains the {@code ChangeInfo} data for the rebased the
+ * chain
+ */
+ default Response<RebaseChainInfo> rebaseChain() throws RestApiException {
+ return rebaseChain(new RebaseInput());
+ }
+
+ /**
+ * Rebase the current revisions of a change's chain.
+ *
+ * @return a {@code RebaseChainInfo} contains the {@code ChangeInfo} data for the rebased the
+ * chain
+ */
+ Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException;
+
/** Deletes a change. */
void delete() throws RestApiException;
@@ -634,6 +654,11 @@
}
@Override
+ public Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public void delete() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 40ae2ec..a865187 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -105,6 +105,7 @@
public Map<String, ActionInfo> actions;
public Map<String, LabelInfo> labels;
public Map<String, Collection<String>> permittedLabels;
+ public Map<String, Map<String, List<AccountInfo>>> removableLabels;
public Collection<AccountInfo> removableReviewers;
public Map<ReviewerState, Collection<AccountInfo>> reviewers;
public Map<ReviewerState, Collection<AccountInfo>> pendingReviewers;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index 24182cc..51c35dc 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -150,16 +150,17 @@
/** Returns {@code null} if nothing has been added to {@code oldCollection} */
@Nullable
private static ImmutableList<?> getAddedForCollection(
- Collection<?> oldCollection, Collection<?> newCollection) {
- ImmutableList<?> notInOldCollection = getAdditions(oldCollection, newCollection);
+ @Nullable Collection<?> oldCollection, Collection<?> newCollection) {
+ ImmutableList<?> notInOldCollection = getAdditionsForCollection(oldCollection, newCollection);
return notInOldCollection.isEmpty() ? null : notInOldCollection;
}
@Nullable
- private static ImmutableList<Object> getAdditions(
- Collection<?> oldCollection, Collection<?> newCollection) {
- if (oldCollection == null)
- return newCollection != null ? ImmutableList.copyOf(newCollection) : null;
+ private static ImmutableList<Object> getAdditionsForCollection(
+ @Nullable Collection<?> oldCollection, Collection<?> newCollection) {
+ if (oldCollection == null) {
+ return ImmutableList.copyOf(newCollection);
+ }
Map<Object, List<Object>> duplicatesMap = newCollection.stream().collect(groupingBy(v -> v));
oldCollection.forEach(
@@ -173,7 +174,18 @@
/** Returns {@code null} if nothing has been added to {@code oldMap} */
@Nullable
- private static ImmutableMap<Object, Object> getAddedForMap(Map<?, ?> oldMap, Map<?, ?> newMap) {
+ private static ImmutableMap<Object, Object> getAddedForMap(
+ @Nullable Map<?, ?> oldMap, Map<?, ?> newMap) {
+ ImmutableMap<Object, Object> notInOldMap = getAdditionsForMap(oldMap, newMap);
+ return notInOldMap.isEmpty() ? null : notInOldMap;
+ }
+
+ @Nullable
+ private static ImmutableMap<Object, Object> getAdditionsForMap(
+ @Nullable Map<?, ?> oldMap, Map<?, ?> newMap) {
+ if (oldMap == null) {
+ return ImmutableMap.copyOf(newMap);
+ }
ImmutableMap.Builder<Object, Object> additionsBuilder = ImmutableMap.builder();
for (Map.Entry<?, ?> entry : newMap.entrySet()) {
Object added = getAdded(oldMap.get(entry.getKey()), entry.getValue());
@@ -181,8 +193,7 @@
additionsBuilder.put(entry.getKey(), added);
}
}
- ImmutableMap<Object, Object> additions = additionsBuilder.build();
- return additions.isEmpty() ? null : additions;
+ return additionsBuilder.build();
}
private static Object get(Field field, Object obj) {
diff --git a/java/com/google/gerrit/extensions/common/RebaseChainInfo.java b/java/com/google/gerrit/extensions/common/RebaseChainInfo.java
new file mode 100644
index 0000000..b327007
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/RebaseChainInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2022 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;
+
+import java.util.List;
+
+public class RebaseChainInfo {
+ public List<ChangeInfo> rebasedChanges;
+ /**
+ * Whether any of the changes contain conflicts.
+ *
+ * <p>If {@code true}, some of the rebased changes are marked with conflicts.
+ */
+ public Boolean containsGitConflicts;
+}
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 0073ec2..df2c5cb 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -55,6 +55,7 @@
import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -308,6 +309,7 @@
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new GerritApiModule());
+ modules.add(new ProjectQueryBuilderModule());
modules.add(new PluginApiModule());
modules.add(new SearchingChangeCacheImplModule());
modules.add(new InternalAccountDirectoryModule());
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 129d961..961bf9b 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -79,7 +79,6 @@
"/dashboard/*",
"/groups/self",
"/settings/*",
- "/topic/*",
"/Documentation/q/*");
/**
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index cc3117d..870d827 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -60,6 +60,9 @@
*/
void replace(V obj);
+ /** Delete a document from the index by value */
+ void deleteByValue(V value);
+
/**
* Delete a document from the index by key.
*
@@ -153,4 +156,14 @@
default boolean isEnabled() {
return true;
}
+
+ /**
+ * Rewriter that should be invoked on queries to this index.
+ *
+ * <p>The default implementation does not do anything. Should be overridden by implementation, if
+ * needed.
+ */
+ default IndexRewriter<V> getIndexRewriter() {
+ return (in, opts) -> in;
+ }
}
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 3114b4c..e050f53 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -15,14 +15,12 @@
package com.google.gerrit.index.project;
import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.index.FieldDef.exact;
-import static com.google.gerrit.index.FieldDef.fullText;
-import static com.google.gerrit.index.FieldDef.prefix;
import static com.google.gerrit.index.FieldDef.storedOnly;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.RefState;
import com.google.gerrit.index.SchemaUtil;
@@ -38,23 +36,53 @@
.toByteArray(project.getNameKey());
}
- public static final FieldDef<ProjectData, String> NAME =
- exact("name").stored().build(p -> p.getProject().getName());
+ public static final IndexedField<ProjectData, String> NAME_FIELD =
+ IndexedField.<ProjectData>stringBuilder("RepoName")
+ .required()
+ .size(200)
+ .stored()
+ .build(p -> p.getProject().getName());
- public static final FieldDef<ProjectData, String> DESCRIPTION =
- fullText("description").stored().build(p -> p.getProject().getDescription());
+ public static final IndexedField<ProjectData, String>.SearchSpec NAME_SPEC =
+ NAME_FIELD.exact("name");
- public static final FieldDef<ProjectData, String> PARENT_NAME =
- exact("parent_name").build(p -> p.getProject().getParentName());
+ public static final IndexedField<ProjectData, String> DESCRIPTION_FIELD =
+ IndexedField.<ProjectData>stringBuilder("Description")
+ .stored()
+ .build(p -> p.getProject().getDescription());
- public static final FieldDef<ProjectData, Iterable<String>> NAME_PART =
- prefix("name_part").buildRepeatable(p -> SchemaUtil.getNameParts(p.getProject().getName()));
+ public static final IndexedField<ProjectData, String>.SearchSpec DESCRIPTION_SPEC =
+ DESCRIPTION_FIELD.fullText("description");
- public static final FieldDef<ProjectData, String> STATE =
- exact("state").stored().build(p -> p.getProject().getState().name());
+ public static final IndexedField<ProjectData, String> PARENT_NAME_FIELD =
+ IndexedField.<ProjectData>stringBuilder("ParentName")
+ .build(p -> p.getProject().getParentName());
- public static final FieldDef<ProjectData, Iterable<String>> ANCESTOR_NAME =
- exact("ancestor_name").buildRepeatable(ProjectData::getParentNames);
+ public static final IndexedField<ProjectData, String>.SearchSpec PARENT_NAME_SPEC =
+ PARENT_NAME_FIELD.exact("parent_name");
+
+ public static final IndexedField<ProjectData, Iterable<String>> NAME_PART_FIELD =
+ IndexedField.<ProjectData>iterableStringBuilder("NamePart")
+ .size(200)
+ .build(p -> SchemaUtil.getNameParts(p.getProject().getName()));
+
+ public static final IndexedField<ProjectData, Iterable<String>>.SearchSpec NAME_PART_SPEC =
+ NAME_PART_FIELD.prefix("name_part");
+
+ public static final IndexedField<ProjectData, String> STATE_FIELD =
+ IndexedField.<ProjectData>stringBuilder("State")
+ .stored()
+ .build(p -> p.getProject().getState().name());
+
+ public static final IndexedField<ProjectData, String>.SearchSpec STATE_SPEC =
+ STATE_FIELD.exact("state");
+
+ public static final IndexedField<ProjectData, Iterable<String>> ANCESTOR_NAME_FIELD =
+ IndexedField.<ProjectData>iterableStringBuilder("AncestorName")
+ .build(ProjectData::getParentNames);
+
+ public static final IndexedField<ProjectData, Iterable<String>>.SearchSpec ANCESTOR_NAME_SPEC =
+ ANCESTOR_NAME_FIELD.exact("ancestor_name");
/**
* All values of all refs that were used in the course of indexing this document. This covers
diff --git a/java/com/google/gerrit/index/project/ProjectIndex.java b/java/com/google/gerrit/index/project/ProjectIndex.java
index b2ddaff..0aa7393 100644
--- a/java/com/google/gerrit/index/project/ProjectIndex.java
+++ b/java/com/google/gerrit/index/project/ProjectIndex.java
@@ -18,6 +18,7 @@
import com.google.gerrit.index.Index;
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.query.Predicate;
+import java.util.function.Function;
/**
* Index for Gerrit projects (repositories). This class is mainly used for typing the generic parent
@@ -30,6 +31,8 @@
@Override
default Predicate<ProjectData> keyPredicate(Project.NameKey nameKey) {
- return new ProjectPredicate(ProjectField.NAME, nameKey.get());
+ return new ProjectPredicate(ProjectField.NAME_SPEC, nameKey.get());
}
+
+ Function<ProjectData, Project.NameKey> ENTITY_TO_KEY = (p) -> p.getProject().getNameKey();
}
diff --git a/java/com/google/gerrit/index/project/ProjectPredicate.java b/java/com/google/gerrit/index/project/ProjectPredicate.java
index 11875ef..0eaf2b6 100644
--- a/java/com/google/gerrit/index/project/ProjectPredicate.java
+++ b/java/com/google/gerrit/index/project/ProjectPredicate.java
@@ -14,12 +14,12 @@
package com.google.gerrit.index.project;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.IndexPredicate;
/** Predicate that is mapped to a field in the project index. */
public class ProjectPredicate extends IndexPredicate<ProjectData> {
- public ProjectPredicate(FieldDef<ProjectData, ?> def, String value) {
+ public ProjectPredicate(SchemaField<ProjectData, ?> def, String value) {
super(def, value);
}
}
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index 0619566..05c23e1 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -16,6 +16,8 @@
import static com.google.gerrit.index.SchemaUtil.schema;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.SchemaDefinitions;
@@ -31,14 +33,26 @@
static final Schema<ProjectData> V1 =
schema(
/* version= */ 1,
- ProjectField.NAME,
- ProjectField.DESCRIPTION,
- ProjectField.PARENT_NAME,
- ProjectField.NAME_PART,
- ProjectField.ANCESTOR_NAME);
+ ImmutableList.of(
+ ProjectField.NAME_FIELD,
+ ProjectField.DESCRIPTION_FIELD,
+ ProjectField.PARENT_NAME_FIELD,
+ ProjectField.NAME_PART_FIELD,
+ ProjectField.ANCESTOR_NAME_FIELD),
+ ImmutableList.<IndexedField<ProjectData, ?>.SearchSpec>of(
+ ProjectField.NAME_SPEC,
+ ProjectField.DESCRIPTION_SPEC,
+ ProjectField.PARENT_NAME_SPEC,
+ ProjectField.NAME_PART_SPEC,
+ ProjectField.ANCESTOR_NAME_SPEC));
@Deprecated
- static final Schema<ProjectData> V2 = schema(V1, ProjectField.STATE, ProjectField.REF_STATE);
+ static final Schema<ProjectData> V2 =
+ schema(
+ V1,
+ ImmutableList.of(ProjectField.REF_STATE),
+ ImmutableList.<IndexedField<ProjectData, ?>>of(ProjectField.STATE_FIELD),
+ ImmutableList.<IndexedField<ProjectData, ?>.SearchSpec>of(ProjectField.STATE_SPEC));
// Bump Lucene version requires reindexing
@Deprecated static final Schema<ProjectData> V3 = schema(V2);
diff --git a/java/com/google/gerrit/index/query/AndPredicate.java b/java/com/google/gerrit/index/query/AndPredicate.java
index 23ae312..fda961d 100644
--- a/java/com/google/gerrit/index/query/AndPredicate.java
+++ b/java/com/google/gerrit/index/query/AndPredicate.java
@@ -134,7 +134,7 @@
cmp = a.estimateCost() - b.estimateCost();
}
- if (cmp == 0 && a instanceof DataSource && b instanceof DataSource) {
+ if (cmp == 0 && a instanceof DataSource) {
DataSource<?> as = (DataSource<?>) a;
DataSource<?> bs = (DataSource<?>) b;
cmp = as.getCardinality() - bs.getCardinality();
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index f237006..1c8bbc3 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -268,7 +268,9 @@
limit,
getRequestedFields());
logger.atFine().log("Query options: %s", opts);
- Predicate<T> pred = rewriter.rewrite(q, opts);
+ // Apply index-specific rewrite first
+ Predicate<T> pred = indexes.getSearchIndex().getIndexRewriter().rewrite(q, opts);
+ pred = rewriter.rewrite(pred, opts);
if (enforceVisibility) {
pred = enforceVisibility(pred);
}
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index d60be14..92bc126 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -279,6 +279,11 @@
@Override
public void insert(ChangeData obj) {}
+
+ @Override
+ public void deleteByValue(ChangeData value) {
+ delete(ChangeIndex.ENTITY_TO_KEY.apply(value));
+ }
}
/** Fake implementation of {@link AccountIndex} where all filtering happens in-memory. */
@@ -311,6 +316,11 @@
@Override
public void insert(AccountState obj) {}
+
+ @Override
+ public void deleteByValue(AccountState value) {
+ delete(AccountIndex.ENTITY_TO_KEY.apply(value));
+ }
}
/** Fake implementation of {@link GroupIndex} where all filtering happens in-memory. */
@@ -344,6 +354,11 @@
@Override
public void insert(InternalGroup obj) {}
+
+ @Override
+ public void deleteByValue(InternalGroup value) {
+ delete(GroupIndex.ENTITY_TO_KEY.apply(value));
+ }
}
/** Fake implementation of {@link ProjectIndex} where all filtering happens in-memory. */
@@ -376,5 +391,10 @@
@Override
public void insert(ProjectData obj) {}
+
+ @Override
+ public void deleteByValue(ProjectData value) {
+ delete(ProjectIndex.ENTITY_TO_KEY.apply(value));
+ }
}
}
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index c158c65..f9dc31a 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -107,6 +107,7 @@
private final Set<NrtFuture> notDoneNrtFutures;
private final AutoFlush autoFlush;
private ScheduledExecutorService autoCommitExecutor;
+ private final Function<V, K> valueToKeyFunction;
@SuppressWarnings("ThreadPriorityCheck")
AbstractLuceneIndex(
@@ -118,7 +119,8 @@
String subIndex,
GerritIndexWriterConfig writerConfig,
SearcherFactory searcherFactory,
- AutoFlush autoFlush)
+ AutoFlush autoFlush,
+ Function<V, K> valueToKeyFunction)
throws IOException {
this.schema = schema;
this.sitePaths = sitePaths;
@@ -126,6 +128,7 @@
this.name = name;
this.skipFields = skipFields;
this.autoFlush = autoFlush;
+ this.valueToKeyFunction = valueToKeyFunction;
String index = Joiner.on('_').skipNulls().join(name, subIndex);
long commitPeriod = writerConfig.getCommitWithinMs();
@@ -299,6 +302,11 @@
}
@Override
+ public void deleteByValue(V value) {
+ delete(valueToKeyFunction.apply(value));
+ }
+
+ @Override
public void deleteAll() {
try {
writer.deleteAll();
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 84cc83a..024b102 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -85,7 +85,8 @@
subIndex,
writerConfig,
searcherFactory,
- autoFlush);
+ autoFlush,
+ ChangeIndex.ENTITY_TO_KEY);
}
@Override
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 2e1771f..9c0baa8 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -108,7 +108,8 @@
null,
new GerritIndexWriterConfig(cfg, ACCOUNTS),
new SearcherFactory(),
- autoFlush);
+ autoFlush,
+ AccountIndex.ENTITY_TO_KEY);
this.accountCache = accountCache;
indexWriterConfig = new GerritIndexWriterConfig(cfg, ACCOUNTS);
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index e16adf5..6365260 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -243,6 +243,11 @@
}
@Override
+ public void deleteByValue(ChangeData value) {
+ delete(ChangeIndex.ENTITY_TO_KEY.apply(value));
+ }
+
+ @Override
public void delete(Change.Id changeId) {
Term idTerm = LuceneChangeIndex.idTerm(changeId);
try {
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index b741042..6301421 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -98,7 +98,8 @@
null,
new GerritIndexWriterConfig(cfg, GROUPS),
new SearcherFactory(),
- autoFlush);
+ autoFlush,
+ GroupIndex.ENTITY_TO_KEY);
this.groupCache = groupCache;
indexWriterConfig = new GerritIndexWriterConfig(cfg, GROUPS);
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 6b2b693..911d91f 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -15,7 +15,7 @@
package com.google.gerrit.lucene;
import static com.google.common.collect.Iterables.getOnlyElement;
-import static com.google.gerrit.index.project.ProjectField.NAME;
+import static com.google.gerrit.index.project.ProjectField.NAME_SPEC;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
@@ -58,14 +58,14 @@
implements ProjectIndex {
private static final String PROJECTS = "projects";
- private static final String NAME_SORT_FIELD = sortFieldName(NAME);
+ private static final String NAME_SORT_FIELD = sortFieldName(NAME_SPEC);
private static Term idTerm(ProjectData projectState) {
return idTerm(projectState.getProject().getNameKey());
}
private static Term idTerm(Project.NameKey nameKey) {
- return QueryBuilder.stringTerm(NAME.getName(), nameKey.get());
+ return QueryBuilder.stringTerm(NAME_SPEC.getName(), nameKey.get());
}
private final GerritIndexWriterConfig indexWriterConfig;
@@ -98,7 +98,8 @@
null,
new GerritIndexWriterConfig(cfg, PROJECTS),
new SearcherFactory(),
- autoFlush);
+ autoFlush,
+ ProjectIndex.ENTITY_TO_KEY);
this.projectCache = projectCache;
indexWriterConfig = new GerritIndexWriterConfig(cfg, PROJECTS);
@@ -109,7 +110,7 @@
void add(Document doc, Values<ProjectData> values) {
// Add separate DocValues field for the field that is needed for sorting.
SchemaField<ProjectData, ?> f = values.getField();
- if (f == NAME) {
+ if (f == NAME_SPEC) {
String value = (String) getOnlyElement(values.getValues());
doc.add(new SortedDocValuesField(NAME_SORT_FIELD, new BytesRef(value)));
}
@@ -155,7 +156,7 @@
@Nullable
@Override
protected ProjectData fromDocument(Document doc) {
- Project.NameKey nameKey = Project.nameKey(doc.getField(NAME.getName()).stringValue());
+ Project.NameKey nameKey = Project.nameKey(doc.getField(NAME_SPEC.getName()).stringValue());
return projectCache.get().get(nameKey).map(ProjectState::toProjectData).orElse(null);
}
}
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 75891fe..0342fe5 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -64,6 +64,7 @@
import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -446,6 +447,7 @@
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new GerritApiModule());
+ modules.add(new ProjectQueryBuilderModule());
modules.add(new PluginApiModule());
modules.add(new SearchingChangeCacheImplModule(replica));
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index 65a81f7..eda6e09 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -100,30 +100,30 @@
enablePeerIPInReflogRecord,
Providers.of(null),
state,
- null);
+ /* realUser= */ null);
}
public IdentifiedUser create(Account.Id id) {
- return create(null, id);
+ return create(/* remotePeer= */ null, id);
}
@VisibleForTesting
@UsedAt(UsedAt.Project.GOOGLE)
public IdentifiedUser forTest(Account.Id id, PropertyMap properties) {
- return runAs(null, id, null, properties);
+ return runAs(/* remotePeer= */ null, id, /* caller= */ null, properties);
}
- public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
- return runAs(remotePeer, id, null);
+ public IdentifiedUser create(@Nullable SocketAddress remotePeer, Account.Id id) {
+ return runAs(remotePeer, id, /* caller= */ null);
}
public IdentifiedUser runAs(
- SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) {
+ @Nullable SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) {
return runAs(remotePeer, id, caller, PropertyMap.EMPTY);
}
private IdentifiedUser runAs(
- SocketAddress remotePeer,
+ @Nullable SocketAddress remotePeer,
Account.Id id,
@Nullable CurrentUser caller,
PropertyMap properties) {
@@ -244,7 +244,7 @@
AccountCache accountCache,
GroupBackend groupBackend,
Boolean enablePeerIPInReflogRecord,
- @Nullable Provider<SocketAddress> remotePeerProvider,
+ Provider<SocketAddress> remotePeerProvider,
AccountState state,
@Nullable CurrentUser realUser) {
this(
@@ -270,7 +270,7 @@
AccountCache accountCache,
GroupBackend groupBackend,
Boolean enablePeerIPInReflogRecord,
- @Nullable Provider<SocketAddress> remotePeerProvider,
+ Provider<SocketAddress> remotePeerProvider,
Account.Id id,
@Nullable CurrentUser realUser,
PropertyMap properties) {
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index 3d449b7..2962108 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -35,11 +35,13 @@
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.update.RepoContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
@@ -169,4 +171,54 @@
return src;
}
}
+
+ /**
+ * Gets the commit ID for the latest patch-set of a given change.
+ *
+ * <p>This also takes into account the patch sets that are added in the provided {@link
+ * RepoContext}.
+ *
+ * @param ctx to look for pending updates in.
+ * @param notesFactory to fetch existing patch sets with.
+ * @param changeId to get the latest commit for.
+ * @return the latest commit ID.
+ * @throws IOException if no committed nor pending commits found for the change.
+ */
+ public static RevCommit getCurrentRevCommitIncludingPending(
+ RepoContext ctx, ChangeNotes.Factory notesFactory, Change.Id changeId) throws IOException {
+ Map<String, ObjectId> refUpdates = ctx.getRepoView().getRefs(changeId.toRefPrefix());
+ refUpdates.remove("meta");
+ if (!refUpdates.isEmpty()) {
+ Optional<PatchSet.Id> latestPendingPatchSet =
+ refUpdates.keySet().stream()
+ .map(r -> PatchSet.Id.fromRef(changeId.toRefPrefix() + r))
+ .max(PatchSet.Id::compareTo);
+ if (latestPendingPatchSet.isPresent()) {
+ return ctx.getRevWalk().parseCommit(refUpdates.get(latestPendingPatchSet.get().getId()));
+ }
+ }
+ return getCurrentCommittedRevCommit(ctx.getProject(), ctx.getRevWalk(), notesFactory, changeId);
+ }
+
+ /**
+ * Gets the commit ID for the latest committed patch-set of a given change.
+ *
+ * <p>This DOES NOT take into account the patch sets that are added in the provided {@link
+ * RepoContext}.
+ *
+ * @param project name.
+ * @param notesFactory to fetch existing patch sets with.
+ * @param changeId to get the latest commit for.
+ * @return the latest commit ID.
+ * @throws IOException if no committed commits found for the change.
+ */
+ public static RevCommit getCurrentCommittedRevCommit(
+ Project.NameKey project,
+ RevWalk revWalk,
+ ChangeNotes.Factory notesFactory,
+ Change.Id changeId)
+ throws IOException {
+ ChangeNotes notes = notesFactory.createChecked(project, changeId);
+ return revWalk.parseCommit(notes.getCurrentPatchSet().commitId());
+ }
}
diff --git a/java/com/google/gerrit/server/PublishCommentsOp.java b/java/com/google/gerrit/server/PublishCommentsOp.java
index 827c078..830928a 100644
--- a/java/com/google/gerrit/server/PublishCommentsOp.java
+++ b/java/com/google/gerrit/server/PublishCommentsOp.java
@@ -23,7 +23,6 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.change.EmailReviewComments;
-import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -112,19 +111,16 @@
}
ChangeNotes changeNotes = changeNotesFactory.createChecked(projectNameKey, psId.changeId());
PatchSet ps = psUtil.get(changeNotes, psId);
- NotifyResolver.Result notify = ctx.getNotify(changeNotes.getChangeId());
- if (notify.shouldNotify()) {
- email
- .create(
- ctx,
- ps,
- preUpdateMetaId,
- mailMessage,
- comments,
- /* patchSetComment= */ null,
- /* labels= */ ImmutableList.of())
- .sendAsync();
- }
+ email
+ .create(
+ ctx,
+ ps,
+ preUpdateMetaId,
+ mailMessage,
+ comments,
+ /* patchSetComment= */ null,
+ /* labels= */ ImmutableList.of())
+ .sendAsync();
commentAdded.fire(
ctx.getChangeData(changeNotes),
ps,
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index fd08fa8..8f413f9 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -23,7 +23,6 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
@@ -39,21 +38,17 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
-import java.util.List;
import java.util.NavigableSet;
+import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -166,20 +161,17 @@
private final GitReferenceUpdated gitRefUpdated;
private final AllUsersName allUsers;
private final Provider<PersonIdent> serverIdent;
- private final Provider<InternalChangeQuery> queryProvider;
@Inject
StarredChangesUtil(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
AllUsersName allUsers,
- @GerritPersonIdent Provider<PersonIdent> serverIdent,
- Provider<InternalChangeQuery> queryProvider) {
+ @GerritPersonIdent Provider<PersonIdent> serverIdent) {
this.repoManager = repoManager;
this.gitRefUpdated = gitRefUpdated;
this.allUsers = allUsers;
this.serverIdent = serverIdent;
- this.queryProvider = queryProvider;
}
public NavigableSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
@@ -238,7 +230,7 @@
batchUpdate.setAllowNonFastForwards(true);
batchUpdate.setRefLogIdent(serverIdent.get());
batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
- for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
+ for (Account.Id accountId : getStars(repo, changeId)) {
String refName = RefNames.refsStarredChanges(changeId, accountId);
Ref ref = repo.getRefDatabase().exactRef(refName);
if (ref != null) {
@@ -264,12 +256,7 @@
public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
try (Repository repo = repoManager.openRepository(allUsers)) {
ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
- for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) {
- Integer id = Ints.tryParse(refPart);
- if (id == null) {
- continue;
- }
- Account.Id accountId = Account.id(id);
+ for (Account.Id accountId : getStars(repo, changeId)) {
builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
}
return builder.build();
@@ -308,22 +295,15 @@
}
}
- public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
- List<ChangeData> changeData =
- queryProvider
- .get()
- .setRequestedFields(ChangeField.CHANGE_ID_SPEC, ChangeField.STAR_SPEC)
- .byLegacyChangeId(changeId);
- if (changeData.size() != 1) {
- throw new NoSuchChangeException(changeId);
- }
- return changeData.get(0).stars();
- }
-
- private static Set<String> getRefNames(Repository repo, String prefix) throws IOException {
- RefDatabase refDb = repo.getRefDatabase();
+ private static Set<Account.Id> getStars(Repository allUsers, Change.Id changeId)
+ throws IOException {
+ String prefix = RefNames.refsStarredChangesPrefix(changeId);
+ RefDatabase refDb = allUsers.getRefDatabase();
return refDb.getRefsByPrefix(prefix).stream()
.map(r -> r.getName().substring(prefix.length()))
+ .map(refPart -> Ints.tryParse(refPart))
+ .filter(Objects::nonNull)
+ .map(id -> Account.id(id))
.collect(toSet());
}
diff --git a/java/com/google/gerrit/server/account/AccountControl.java b/java/com/google/gerrit/server/account/AccountControl.java
index c80059b..ca63565 100644
--- a/java/com/google/gerrit/server/account/AccountControl.java
+++ b/java/com/google/gerrit/server/account/AccountControl.java
@@ -82,12 +82,12 @@
* accounts.
*/
@UsedAt(UsedAt.Project.PLUGIN_CODE_OWNERS)
- public AccountControl get(IdentifiedUser identifiedUser) {
+ public AccountControl get(CurrentUser user) {
return new AccountControl(
permissionBackend,
projectCache,
groupControlFactory,
- identifiedUser,
+ user,
userFactory,
accountVisibility);
}
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 65eb332..fcfc805 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -132,12 +132,18 @@
private final String input;
private final ImmutableList<AccountState> list;
private final ImmutableList<AccountState> filteredInactive;
+ private final CurrentUser searchedAsUser;
@VisibleForTesting
- Result(String input, List<AccountState> list, List<AccountState> filteredInactive) {
+ Result(
+ String input,
+ List<AccountState> list,
+ List<AccountState> filteredInactive,
+ CurrentUser searchedAsUser) {
this.input = requireNonNull(input);
this.list = canonicalize(list);
this.filteredInactive = canonicalize(filteredInactive);
+ this.searchedAsUser = requireNonNull(searchedAsUser);
}
private ImmutableList<AccountState> canonicalize(List<AccountState> list) {
@@ -180,13 +186,21 @@
}
}
- public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
+ private void ensureSelfIsUniqueIdentifiedUser() throws UnresolvableAccountException {
ensureUnique();
+ if (!searchedAsUser.isIdentifiedUser()) {
+ throw new UnresolvableAccountException(this);
+ }
+ }
+
+ public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
if (isSelf()) {
+ ensureSelfIsUniqueIdentifiedUser();
// In the special case of "self", use the exact IdentifiedUser from the request context, to
// preserve the peer address and any other per-request state.
- return self.get().asIdentifiedUser();
+ return searchedAsUser.asIdentifiedUser();
}
+ ensureUnique();
return userFactory.create(asUnique());
}
@@ -194,11 +208,10 @@
throws UnresolvableAccountException {
ensureUnique();
if (isSelf()) {
- // TODO(dborowitz): This preserves old behavior, but it seems wrong to discard the caller.
- return self.get().asIdentifiedUser();
+ return searchedAsUser.asIdentifiedUser();
}
return userFactory.runAs(
- null, list.get(0).account().id(), requireNonNull(caller).getRealUser());
+ /* remotePeer= */ null, list.get(0).account().id(), requireNonNull(caller).getRealUser());
}
@VisibleForTesting
@@ -221,16 +234,57 @@
return false;
}
+ /**
+ * Searches can be done on behalf of either the current user or another provided user. The
+ * results of some searchers, such as BySelf, are affected by the context user.
+ */
+ default boolean requiresContextUser() {
+ return false;
+ }
+
Optional<I> tryParse(String input) throws IOException;
- Stream<AccountState> search(I input) throws IOException, ConfigInvalidException;
+ /**
+ * This method should be implemented for every searcher which doesn't require a context user.
+ *
+ * @param input to search for
+ * @return stream of the matching accounts
+ * @throws IOException by some subclasses
+ * @throws ConfigInvalidException by some subclasses
+ */
+ default Stream<AccountState> search(I input) throws IOException, ConfigInvalidException {
+ throw new IllegalStateException("search(I) default implementation should never be called.");
+ }
+
+ /**
+ * This method should be implemented for every searcher which requires a context user.
+ *
+ * @param input to search for
+ * @param asUser the context user for the search
+ * @return stream of the matching accounts
+ * @throws IOException by some subclasses
+ * @throws ConfigInvalidException by some subclasses
+ */
+ default Stream<AccountState> search(I input, CurrentUser asUser)
+ throws IOException, ConfigInvalidException {
+ if (!requiresContextUser()) {
+ return search(input);
+ }
+ throw new IllegalStateException(
+ "The searcher requires a context user, but doesn't implement search(input, asUser).");
+ }
boolean shortCircuitIfNoResults();
- default Optional<Stream<AccountState>> trySearch(String input)
+ default Optional<Stream<AccountState>> trySearch(String input, CurrentUser asUser)
throws IOException, ConfigInvalidException {
Optional<I> parsed = tryParse(input);
- return parsed.isPresent() ? Optional.of(search(parsed.get())) : Optional.empty();
+ if (parsed.isEmpty()) {
+ return Optional.empty();
+ }
+ return requiresContextUser()
+ ? Optional.of(search(parsed.get(), asUser))
+ : Optional.of(search(parsed.get()));
}
}
@@ -251,7 +305,7 @@
}
}
- private class BySelf extends StringSearcher {
+ private static class BySelf extends StringSearcher {
@Override
public boolean callerShouldFilterOutInactiveCandidates() {
return false;
@@ -263,17 +317,21 @@
}
@Override
+ public boolean requiresContextUser() {
+ return true;
+ }
+
+ @Override
protected boolean matches(String input) {
return "self".equals(input) || "me".equals(input);
}
@Override
- public Stream<AccountState> search(String input) {
- CurrentUser user = self.get();
- if (!user.isIdentifiedUser()) {
+ public Stream<AccountState> search(String input, CurrentUser asUser) {
+ if (!asUser.isIdentifiedUser()) {
return Stream.empty();
}
- return Stream.of(user.asIdentifiedUser().state());
+ return Stream.of(asUser.asIdentifiedUser().state());
}
@Override
@@ -400,9 +458,20 @@
}
private class ByFullName implements Searcher<AccountState> {
+ boolean allowSkippingVisibilityCheck = true;
+
+ ByFullName() {
+ super();
+ }
+
+ ByFullName(boolean allowSkippingVisibilityCheck) {
+ this();
+ this.allowSkippingVisibilityCheck = allowSkippingVisibilityCheck;
+ }
+
@Override
public boolean callerMayAssumeCandidatesAreVisible() {
- return true; // Rely on enforceVisibility from the index.
+ return allowSkippingVisibilityCheck;
}
@Override
@@ -424,9 +493,25 @@
}
private class ByDefaultSearch extends StringSearcher {
+ boolean allowSkippingVisibilityCheck = true;
+
+ ByDefaultSearch() {
+ super();
+ }
+
+ ByDefaultSearch(boolean allowSkippingVisibilityCheck) {
+ this();
+ this.allowSkippingVisibilityCheck = allowSkippingVisibilityCheck;
+ }
+
@Override
public boolean callerMayAssumeCandidatesAreVisible() {
- return true; // Rely on enforceVisibility from the index.
+ return allowSkippingVisibilityCheck;
+ }
+
+ @Override
+ public boolean requiresContextUser() {
+ return true;
}
@Override
@@ -435,14 +520,14 @@
}
@Override
- public Stream<AccountState> search(String input) {
+ public Stream<AccountState> search(String input, CurrentUser asUser) {
// At this point we have no clue. Just perform a whole bunch of suggestions and pray we come
// up with a reasonable result list.
// TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
// more strict here.
boolean canSeeSecondaryEmails = false;
try {
- if (permissionBackend.user(self.get()).test(GlobalPermission.MODIFY_ACCOUNT)) {
+ if (permissionBackend.user(asUser).test(GlobalPermission.MODIFY_ACCOUNT)) {
canSeeSecondaryEmails = true;
}
} catch (PermissionBackendException e) {
@@ -477,6 +562,18 @@
.addAll(nameOrEmailSearchers)
.build();
+ private final ImmutableList<Searcher<?>> forcedVisibilitySearchers =
+ ImmutableList.of(
+ new ByNameAndEmail(),
+ new ByEmail(),
+ new FromRealm(),
+ new ByFullName(false),
+ new ByDefaultSearch(false),
+ new BySelf(),
+ new ByExactAccountId(),
+ new ByParenthesizedAccountId(),
+ new ByUsername());
+
private final AccountCache accountCache;
private final AccountControl.Factory accountControlFactory;
private final Emails emails;
@@ -538,12 +635,63 @@
* @throws IOException if an error occurs.
*/
public Result resolve(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::isActive);
+ return searchImpl(
+ input, searchers, self.get(), this::currentUserCanSeePredicate, AccountResolver::isActive);
}
public Result resolve(String input, Predicate<AccountState> accountActivityPredicate)
throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::canSeePredicate, accountActivityPredicate);
+ return searchImpl(
+ input, searchers, self.get(), this::currentUserCanSeePredicate, accountActivityPredicate);
+ }
+
+ /**
+ * Resolves all accounts matching the input string, visible to the provided user.
+ *
+ * <p>The following input formats are recognized:
+ *
+ * <ul>
+ * <li>The strings {@code "self"} and {@code "me"}, if the provided user is an {@link
+ * IdentifiedUser}. In this case, may return exactly one inactive account.
+ * <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive
+ * account. This case short-circuits if the input matches.
+ * <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This
+ * case short-circuits if the input matches.
+ * <li>A username ({@code "username"}).
+ * <li>A full name and email address ({@code "Full Name <email@example>"}). This case
+ * short-circuits if the input matches.
+ * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
+ * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
+ * <li>A full name ({@code "Full Name"}).
+ * <li>As a fallback, a {@link
+ * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
+ * boolean, String) default search} against the account index.
+ * </ul>
+ *
+ * @param asUser user to resolve the users by.
+ * @param input input string.
+ * @param forceVisibilityCheck whether to force all searchers to check for visibility.
+ * @return a result describing matching accounts. Never null even if the result set is empty.
+ * @throws ConfigInvalidException if an error occurs.
+ * @throws IOException if an error occurs.
+ */
+ public Result resolveAsUser(CurrentUser asUser, String input, boolean forceVisibilityCheck)
+ throws ConfigInvalidException, IOException {
+ return resolveAsUser(asUser, input, AccountResolver::isActive, forceVisibilityCheck);
+ }
+
+ public Result resolveAsUser(
+ CurrentUser asUser,
+ String input,
+ Predicate<AccountState> accountActivityPredicate,
+ boolean forceVisibilityCheck)
+ throws ConfigInvalidException, IOException {
+ return searchImpl(
+ input,
+ forceVisibilityCheck ? forcedVisibilitySearchers : searchers,
+ asUser,
+ new ProvidedUserCanSeePredicate(asUser),
+ accountActivityPredicate);
}
/**
@@ -556,22 +704,23 @@
* instead will be stored as a link to the corresponding Gerrit Account.
*/
public Result resolveIncludeInactive(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::allVisible);
+ return searchImpl(
+ input,
+ searchers,
+ self.get(),
+ this::currentUserCanSeePredicate,
+ AccountResolver::allVisible);
}
public Result resolveIncludeInactiveIgnoreVisibility(String input)
throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::allVisible);
+ return searchImpl(
+ input, searchers, self.get(), this::allVisiblePredicate, AccountResolver::allVisible);
}
public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::isActive);
- }
-
- public Result resolveIgnoreVisibility(
- String input, Predicate<AccountState> accountActivityPredicate)
- throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::allVisiblePredicate, accountActivityPredicate);
+ return searchImpl(
+ input, searchers, self.get(), this::allVisiblePredicate, AccountResolver::isActive);
}
/**
@@ -600,7 +749,11 @@
@Deprecated
public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException {
return searchImpl(
- input, nameOrEmailSearchers, this::canSeePredicate, AccountResolver::isActive);
+ input,
+ nameOrEmailSearchers,
+ self.get(),
+ this::currentUserCanSeePredicate,
+ AccountResolver::isActive);
}
/**
@@ -619,16 +772,26 @@
return searchImpl(
input,
ImmutableList.of(new ByNameAndEmail(), new ByEmail(), new ByFullName(), new ByUsername()),
- this::canSeePredicate,
+ self.get(),
+ this::currentUserCanSeePredicate,
AccountResolver::isActive);
}
- private Predicate<AccountState> canSeePredicate() {
- return this::canSee;
+ private Predicate<AccountState> currentUserCanSeePredicate() {
+ return accountControlFactory.get()::canSee;
}
- private boolean canSee(AccountState accountState) {
- return accountControlFactory.get().canSee(accountState);
+ private class ProvidedUserCanSeePredicate implements Supplier<Predicate<AccountState>> {
+ CurrentUser asUser;
+
+ ProvidedUserCanSeePredicate(CurrentUser asUser) {
+ this.asUser = asUser;
+ }
+
+ @Override
+ public Predicate<AccountState> get() {
+ return accountControlFactory.get(asUser.asIdentifiedUser())::canSee;
+ }
}
private Predicate<AccountState> allVisiblePredicate() {
@@ -648,14 +811,16 @@
Result searchImpl(
String input,
ImmutableList<Searcher<?>> searchers,
+ CurrentUser asUser,
Supplier<Predicate<AccountState>> visibilitySupplier,
Predicate<AccountState> accountActivityPredicate)
throws ConfigInvalidException, IOException {
+ requireNonNull(asUser);
visibilitySupplier = Suppliers.memoize(visibilitySupplier::get);
List<AccountState> inactive = new ArrayList<>();
for (Searcher<?> searcher : searchers) {
- Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input);
+ Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input, asUser);
if (!maybeResults.isPresent()) {
continue;
}
@@ -677,22 +842,25 @@
}
if (!list.isEmpty()) {
- return createResult(input, list);
+ return createResult(input, list, asUser);
}
if (searcher.shortCircuitIfNoResults()) {
// For a short-circuiting searcher, return results even if empty.
- return !inactive.isEmpty() ? emptyResult(input, inactive) : createResult(input, list);
+ return !inactive.isEmpty()
+ ? emptyResult(input, inactive, asUser)
+ : createResult(input, list, asUser);
}
}
- return emptyResult(input, inactive);
+ return emptyResult(input, inactive, asUser);
}
- private Result createResult(String input, List<AccountState> list) {
- return new Result(input, list, ImmutableList.of());
+ private Result createResult(String input, List<AccountState> list, CurrentUser searchedAsUser) {
+ return new Result(input, list, ImmutableList.of(), searchedAsUser);
}
- private Result emptyResult(String input, List<AccountState> inactive) {
- return new Result(input, ImmutableList.of(), inactive);
+ private Result emptyResult(
+ String input, List<AccountState> inactive, CurrentUser searchedAsUser) {
+ return new Result(input, ImmutableList.of(), inactive, searchedAsUser);
}
private Stream<AccountState> toAccountStates(Set<Account.Id> ids) {
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 8d5fea4..d6ea294 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -202,7 +202,6 @@
private ExternalIdNotes externalIdNotes;
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
AccountsUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
@@ -228,7 +227,6 @@
}
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
AccountsUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
diff --git a/java/com/google/gerrit/server/account/GroupCache.java b/java/com/google/gerrit/server/account/GroupCache.java
index 1e28d7d..46c730c 100644
--- a/java/com/google/gerrit/server/account/GroupCache.java
+++ b/java/com/google/gerrit/server/account/GroupCache.java
@@ -14,11 +14,15 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.exceptions.StorageException;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
/** Tracks group objects in memory for efficient access. */
public interface GroupCache {
@@ -62,6 +66,22 @@
Map<AccountGroup.UUID, InternalGroup> get(Collection<AccountGroup.UUID> groupUuids);
/**
+ * Returns an {@code InternalGroup} instance for the given {@code AccountGroup.UUID} at the given
+ * {@code metaId} of {@link com.google.gerrit.entities.RefNames#refsGroups} ref.
+ *
+ * <p>The caller is responsible to ensure the presence of {@code metaId} and the corresponding
+ * meta ref.
+ *
+ * @param groupUuid the UUID of the internal group
+ * @param metaId the sha1 of commit in {@link com.google.gerrit.entities.RefNames#refsGroups} ref.
+ * @return the internal group at specific sha1 {@code metaId}
+ * @throws StorageException if no internal group with this UUID exists on this server at the
+ * specific sha1, or if an error occurred during lookup.
+ */
+ @UsedAt(Project.GOOGLE)
+ InternalGroup getFromMetaId(AccountGroup.UUID groupUuid, ObjectId metaId) throws StorageException;
+
+ /**
* Removes the association of the given ID with a group.
*
* <p>The next call to {@link #get(AccountGroup.Id)} won't provide a cached value.
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 2d947ba..6f4fce9 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -27,6 +27,7 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.proto.Cache;
@@ -122,15 +123,19 @@
private final LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId;
private final LoadingCache<String, Optional<InternalGroup>> byName;
private final LoadingCache<String, Optional<InternalGroup>> byUUID;
+ private final LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedByUuidCache;
@Inject
GroupCacheImpl(
@Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId,
@Named(BYNAME_NAME) LoadingCache<String, Optional<InternalGroup>> byName,
- @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID) {
+ @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID,
+ @Named(BYUUID_NAME_PERSISTED)
+ LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedByUuidCache) {
this.byId = byId;
this.byName = byName;
this.byUUID = byUUID;
+ this.persistedByUuidCache = persistedByUuidCache;
}
@Override
@@ -185,6 +190,21 @@
}
@Override
+ public InternalGroup getFromMetaId(AccountGroup.UUID groupUuid, ObjectId metaId)
+ throws StorageException {
+ Cache.GroupKeyProto key =
+ Cache.GroupKeyProto.newBuilder()
+ .setUuid(groupUuid.get())
+ .setRevision(ObjectIdConverter.create().toByteString(metaId))
+ .build();
+ try {
+ return persistedByUuidCache.get(key);
+ } catch (ExecutionException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
public void evict(AccountGroup.Id groupId) {
if (groupId != null) {
logger.atFine().log("Evict group %s by ID", groupId.get());
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index e0569f4..66a845a 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -54,6 +54,7 @@
import com.google.gerrit.extensions.common.InputWithMessage;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.common.RebaseChainInfo;
import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInput;
@@ -100,6 +101,7 @@
import com.google.gerrit.server.restapi.change.PutMessage;
import com.google.gerrit.server.restapi.change.PutTopic;
import com.google.gerrit.server.restapi.change.Rebase;
+import com.google.gerrit.server.restapi.change.RebaseChain;
import com.google.gerrit.server.restapi.change.Restore;
import com.google.gerrit.server.restapi.change.Revert;
import com.google.gerrit.server.restapi.change.RevertSubmission;
@@ -144,6 +146,7 @@
private final ApplyPatch applyPatch;
private final Provider<SubmittedTogether> submittedTogether;
private final Rebase.CurrentRevision rebase;
+ private final RebaseChain rebaseChain;
private final DeleteChange deleteChange;
private final GetTopic getTopic;
private final PutTopic putTopic;
@@ -197,6 +200,7 @@
ApplyPatch applyPatch,
Provider<SubmittedTogether> submittedTogether,
Rebase.CurrentRevision rebase,
+ RebaseChain rebaseChain,
DeleteChange deleteChange,
GetTopic getTopic,
PutTopic putTopic,
@@ -248,6 +252,7 @@
this.applyPatch = applyPatch;
this.submittedTogether = submittedTogether;
this.rebase = rebase;
+ this.rebaseChain = rebaseChain;
this.deleteChange = deleteChange;
this.getTopic = getTopic;
this.putTopic = putTopic;
@@ -427,6 +432,15 @@
}
@Override
+ public Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException {
+ try {
+ return rebaseChain.apply(change, in);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot rebase chain", e);
+ }
+ }
+
+ @Override
public void delete() throws RestApiException {
try {
deleteChange.apply(change, null);
diff --git a/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java b/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java
new file mode 100644
index 0000000..8ed1175
--- /dev/null
+++ b/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2023 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.api.projects;
+
+import com.google.gerrit.server.query.project.ProjectQueryBuilder;
+import com.google.gerrit.server.query.project.ProjectQueryBuilderImpl;
+import com.google.inject.AbstractModule;
+
+public class ProjectQueryBuilderModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(ProjectQueryBuilder.class).to(ProjectQueryBuilderImpl.class);
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 2883ef8..8773bb7 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -525,7 +525,7 @@
public void postUpdate(PostUpdateContext ctx) throws Exception {
reviewerAdditions.postUpdate(ctx);
NotifyResolver.Result notify = ctx.getNotify(change.getId());
- if (sendMail && notify.shouldNotify()) {
+ if (sendMail) {
Runnable sender =
new Runnable() {
@Override
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 500bb77..912d202 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -146,7 +146,7 @@
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build();
- static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
+ public static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
ImmutableSet.of(
ALL_COMMITS,
ALL_REVISIONS,
@@ -688,6 +688,7 @@
!cd.change().isAbandoned()
? labelsJson.permittedLabels(user.getAccountId(), cd)
: ImmutableMap.of();
+ out.removableLabels = labelsJson.removableLabels(accountLoader, user, cd);
}
}
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index b512a2d..f3fd68e 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -76,9 +76,6 @@
if (sendEmail) {
try {
NotifyResolver.Result notify = ctx.getNotify(change.getId());
- if (!notify.shouldNotify()) {
- return;
- }
DeleteReviewerSender emailSender =
deleteReviewerSenderFactory.create(ctx.getProject(), change.getId());
emailSender.setFrom(ctx.getAccountId());
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 1199be5..afb9d76 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -193,15 +193,13 @@
notify = notify.withHandling(NotifyHandling.OWNER);
}
try {
- if (notify.shouldNotify()) {
- emailReviewers(
- ctx.getProject(),
- currChange,
- mailMessage,
- Timestamp.from(ctx.getWhen()),
- notify,
- ctx.getRepoView());
- }
+ emailReviewers(
+ ctx.getProject(),
+ currChange,
+ mailMessage,
+ Timestamp.from(ctx.getWhen()),
+ notify,
+ ctx.getRepoView());
} catch (Exception err) {
logger.atSevere().withCause(err).log(
"Cannot email update for change %s", currChange.getId());
diff --git a/java/com/google/gerrit/server/change/EmailNewPatchSet.java b/java/com/google/gerrit/server/change/EmailNewPatchSet.java
index f6ae6a3..f67ce4a 100644
--- a/java/com/google/gerrit/server/change/EmailNewPatchSet.java
+++ b/java/com/google/gerrit/server/change/EmailNewPatchSet.java
@@ -16,6 +16,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -52,7 +53,7 @@
EmailNewPatchSet create(
PostUpdateContext postUpdateContext,
PatchSet patchSet,
- String message,
+ @Nullable String message,
ImmutableSet<PatchSetApproval> outdatedApprovals,
@Assisted("reviewers") ImmutableSet<Account.Id> reviewers,
@Assisted("extraCcs") ImmutableSet<Account.Id> extraCcs,
@@ -75,7 +76,7 @@
MessageIdGenerator messageIdGenerator,
@Assisted PostUpdateContext postUpdateContext,
@Assisted PatchSet patchSet,
- @Assisted String message,
+ @Nullable @Assisted String message,
@Assisted ImmutableSet<PatchSetApproval> outdatedApprovals,
@Assisted("reviewers") ImmutableSet<Account.Id> reviewers,
@Assisted("extraCcs") ImmutableSet<Account.Id> extraCcs,
diff --git a/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
index 194a4f0..9a75469 100644
--- a/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
+++ b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
@@ -65,6 +65,33 @@
*/
public List<RelatedChangesSorter.PatchSetData> getRelated(ChangeData changeData, PatchSet basePs)
throws IOException, PermissionBackendException {
+ List<ChangeData> cds = getUnsortedRelated(changeData, basePs, false);
+ if (cds.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return sorter.sort(cds, basePs);
+ }
+
+ /**
+ * Gets ancestor changes of a specific change revision.
+ *
+ * @param changeData the change of the inputted revision.
+ * @param basePs the revision that the method checks for related changes.
+ * @param alwaysIncludeOriginalChange whether to return the given change when no ancestors found.
+ * @return list of ancestor changes, sorted via {@link RelatedChangesSorter}
+ */
+ public List<RelatedChangesSorter.PatchSetData> getAncestors(
+ ChangeData changeData, PatchSet basePs, boolean alwaysIncludeOriginalChange)
+ throws IOException, PermissionBackendException {
+ List<ChangeData> cds = getUnsortedRelated(changeData, basePs, alwaysIncludeOriginalChange);
+ if (cds.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return sorter.sortAncestors(cds, basePs);
+ }
+
+ private List<ChangeData> getUnsortedRelated(
+ ChangeData changeData, PatchSet basePs, boolean alwaysIncludeOriginalChange) {
Set<String> groups = getAllGroups(changeData.patchSets());
logger.atFine().log("groups = %s", groups);
if (groups.isEmpty()) {
@@ -78,12 +105,10 @@
return Collections.emptyList();
}
if (cds.size() == 1 && cds.get(0).getId().equals(changeData.getId())) {
- return Collections.emptyList();
+ return alwaysIncludeOriginalChange ? cds : Collections.emptyList();
}
- cds = reloadChangeIfStale(cds, changeData, basePs);
-
- return sorter.sort(cds, basePs);
+ return reloadChangeIfStale(cds, changeData, basePs);
}
private List<ChangeData> reloadChangeIfStale(
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index cfa15ae..5555ba6 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -36,15 +36,19 @@
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.VotingRangeInfo;
import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.DeleteVoteControl;
+import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -69,10 +73,17 @@
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final PermissionBackend permissionBackend;
+ private final DeleteVoteControl deleteVoteControl;
+ private final RemoveReviewerControl removeReviewerControl;
@Inject
- LabelsJson(PermissionBackend permissionBackend) {
+ LabelsJson(
+ PermissionBackend permissionBackend,
+ DeleteVoteControl deleteVoteControl,
+ RemoveReviewerControl removeReviewerControl) {
this.permissionBackend = permissionBackend;
+ this.deleteVoteControl = deleteVoteControl;
+ this.removeReviewerControl = removeReviewerControl;
}
/**
@@ -133,6 +144,46 @@
return permitted.asMap();
}
+ /**
+ * Returns A map of all labels that the provided user has permission to remove.
+ *
+ * @param accountLoader to load the reviewers' data with.
+ * @param user a Gerrit user.
+ * @param cd {@link ChangeData} corresponding to a specific gerrit change.
+ * @return A Map of {@code labelName} -> {Map of {@code value} -> List of {@link AccountInfo}}
+ * that the user can remove votes from.
+ */
+ Map<String, Map<String, List<AccountInfo>>> removableLabels(
+ AccountLoader accountLoader, CurrentUser user, ChangeData cd)
+ throws PermissionBackendException {
+ if (cd.change().isMerged()) {
+ return new HashMap<>();
+ }
+
+ Map<String, Map<String, List<AccountInfo>>> res = new HashMap<>();
+ LabelTypes labelTypes = cd.getLabelTypes();
+ for (PatchSetApproval approval : cd.currentApprovals()) {
+ Optional<LabelType> labelType = labelTypes.byLabel(approval.labelId());
+ if (!labelType.isPresent()) {
+ continue;
+ }
+ if (!(deleteVoteControl.testDeleteVotePermissions(user, cd, approval, labelType.get())
+ || removeReviewerControl.testRemoveReviewer(
+ cd, user, approval.accountId(), approval.value()))) {
+ continue;
+ }
+ if (!res.containsKey(approval.label())) {
+ res.put(approval.label(), new HashMap<>());
+ }
+ String labelValue = LabelValue.formatValue(approval.value());
+ if (!res.get(approval.label()).containsKey(labelValue)) {
+ res.get(approval.label()).put(labelValue, new ArrayList<>());
+ }
+ res.get(approval.label()).get(labelValue).add(accountLoader.get(approval.accountId()));
+ }
+ return res;
+ }
+
private static void clearOnlyZerosEntries(SetMultimap<String, String> permitted) {
List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) {
@@ -217,10 +268,10 @@
}
}
- private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd) {
+ private Map<String, Short> currentLabels(@Nullable Account.Id accountId, ChangeData cd) {
Map<String, Short> result = new HashMap<>();
for (PatchSetApproval psa : cd.currentApprovals()) {
- if (psa.accountId().equals(accountId)) {
+ if (accountId == null || psa.accountId().equals(accountId)) {
result.put(psa.label(), psa.value());
}
}
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 1fe67af..cff3de2 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -22,6 +22,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -361,9 +362,7 @@
@Override
public void postUpdate(PostUpdateContext ctx) {
NotifyResolver.Result notify = ctx.getNotify(change.getId());
- if (notify.shouldNotify() && sendEmail) {
- requireNonNull(mailMessage);
-
+ if (sendEmail) {
emailNewPatchSetFactory
.create(
ctx,
@@ -372,8 +371,8 @@
approvalCopierResult.outdatedApprovals().stream()
.map(ApprovalCopier.Result.PatchSetApprovalData::patchSetApproval)
.collect(toImmutableSet()),
- oldReviewers.byState(REVIEWER),
- oldReviewers.byState(CC),
+ oldReviewers == null ? ImmutableSet.of() : oldReviewers.byState(REVIEWER),
+ oldReviewers == null ? ImmutableSet.of() : oldReviewers.byState(CC),
changeKind,
preUpdateMetaId)
.sendAsync();
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 4de21d6..49ec812 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -22,7 +22,9 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -30,6 +32,8 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.RebaseUtil.Base;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -46,8 +50,8 @@
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.PostUpdateContext;
import com.google.gerrit.server.update.RepoContext;
-import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -73,19 +77,24 @@
public class RebaseChangeOp implements BatchUpdateOp {
public interface Factory {
RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, ObjectId baseCommitId);
+
+ RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, Change.Id baseChangeId);
}
private final PatchSetInserter.Factory patchSetInserterFactory;
private final MergeUtilFactory mergeUtilFactory;
private final RebaseUtil rebaseUtil;
private final ChangeResource.Factory changeResourceFactory;
+ private final ChangeNotes.Factory notesFactory;
private final ChangeNotes notes;
private final PatchSet originalPatchSet;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final ProjectCache projectCache;
+ private final Project.NameKey projectName;
private ObjectId baseCommitId;
+ private Change.Id baseChangeId;
private PersonIdent committerIdent;
private boolean fireRevisionCreated = true;
private boolean validate = true;
@@ -104,26 +113,78 @@
private PatchSetInserter patchSetInserter;
private PatchSet rebasedPatchSet;
- @Inject
+ @AssistedInject
RebaseChangeOp(
PatchSetInserter.Factory patchSetInserterFactory,
MergeUtilFactory mergeUtilFactory,
RebaseUtil rebaseUtil,
ChangeResource.Factory changeResourceFactory,
- IdentifiedUser.GenericFactory identifiedUserFactory,
+ ChangeNotes.Factory notesFactory,
+ GenericFactory identifiedUserFactory,
ProjectCache projectCache,
@Assisted ChangeNotes notes,
@Assisted PatchSet originalPatchSet,
@Assisted ObjectId baseCommitId) {
+ this(
+ patchSetInserterFactory,
+ mergeUtilFactory,
+ rebaseUtil,
+ changeResourceFactory,
+ notesFactory,
+ identifiedUserFactory,
+ projectCache,
+ notes,
+ originalPatchSet);
+ this.baseCommitId = baseCommitId;
+ this.baseChangeId = null;
+ }
+
+ @AssistedInject
+ RebaseChangeOp(
+ PatchSetInserter.Factory patchSetInserterFactory,
+ MergeUtilFactory mergeUtilFactory,
+ RebaseUtil rebaseUtil,
+ ChangeResource.Factory changeResourceFactory,
+ ChangeNotes.Factory notesFactory,
+ GenericFactory identifiedUserFactory,
+ ProjectCache projectCache,
+ @Assisted ChangeNotes notes,
+ @Assisted PatchSet originalPatchSet,
+ @Assisted Change.Id baseChangeId) {
+ this(
+ patchSetInserterFactory,
+ mergeUtilFactory,
+ rebaseUtil,
+ changeResourceFactory,
+ notesFactory,
+ identifiedUserFactory,
+ projectCache,
+ notes,
+ originalPatchSet);
+ this.baseChangeId = baseChangeId;
+ this.baseCommitId = null;
+ }
+
+ private RebaseChangeOp(
+ PatchSetInserter.Factory patchSetInserterFactory,
+ MergeUtilFactory mergeUtilFactory,
+ RebaseUtil rebaseUtil,
+ ChangeResource.Factory changeResourceFactory,
+ ChangeNotes.Factory notesFactory,
+ GenericFactory identifiedUserFactory,
+ ProjectCache projectCache,
+ ChangeNotes notes,
+ PatchSet originalPatchSet) {
this.patchSetInserterFactory = patchSetInserterFactory;
this.mergeUtilFactory = mergeUtilFactory;
this.rebaseUtil = rebaseUtil;
this.changeResourceFactory = changeResourceFactory;
+ this.notesFactory = notesFactory;
this.identifiedUserFactory = identifiedUserFactory;
this.projectCache = projectCache;
this.notes = notes;
+ this.projectName = notes.getProjectName();
this.originalPatchSet = originalPatchSet;
- this.baseCommitId = baseCommitId;
}
public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) {
@@ -204,14 +265,23 @@
@Override
public void updateRepo(RepoContext ctx)
- throws MergeConflictException, InvalidChangeOperationException, RestApiException, IOException,
- NoSuchChangeException, PermissionBackendException {
+ throws InvalidChangeOperationException, RestApiException, IOException, NoSuchChangeException,
+ PermissionBackendException {
// Ok that originalPatchSet was not read in a transaction, since we just
// need its revision.
RevWalk rw = ctx.getRevWalk();
RevCommit original = rw.parseCommit(originalPatchSet.commitId());
rw.parseBody(original);
- RevCommit baseCommit = rw.parseCommit(baseCommitId);
+ RevCommit baseCommit;
+ if (baseCommitId != null && baseChangeId == null) {
+ baseCommit = rw.parseCommit(baseCommitId);
+ } else if (baseChangeId != null) {
+ baseCommit =
+ PatchSetUtil.getCurrentRevCommitIncludingPending(ctx, notesFactory, baseChangeId);
+ } else {
+ throw new IllegalStateException(
+ "Exactly one of base commit and base change must be provided.");
+ }
CurrentUser changeOwner = identifiedUserFactory.create(notes.getChange().getOwner());
String newCommitMessage;
@@ -224,12 +294,12 @@
newCommitMessage = original.getFullMessage();
}
- rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage);
+ rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage, notes.getChangeId());
Base base =
rebaseUtil.parseBase(
new RevisionResource(
changeResourceFactory.create(notes, changeOwner), originalPatchSet),
- baseCommitId.name());
+ baseCommit.getName());
rebasedPatchSetId =
ChangeUtil.nextPatchSetIdFromChangeRefs(
@@ -320,8 +390,7 @@
}
private MergeUtil newMergeUtil() {
- ProjectState project =
- projectCache.get(notes.getProjectName()).orElseThrow(illegalState(notes.getProjectName()));
+ ProjectState project = projectCache.get(projectName).orElseThrow(illegalState(projectName));
return forceContentMerge
? mergeUtilFactory.create(project, true)
: mergeUtilFactory.create(project);
@@ -338,7 +407,11 @@
* @throws IOException the merge failed for another reason.
*/
private CodeReviewCommit rebaseCommit(
- RepoContext ctx, RevCommit original, ObjectId base, String commitMessage)
+ RepoContext ctx,
+ RevCommit original,
+ ObjectId base,
+ String commitMessage,
+ Change.Id originalChangeId)
throws ResourceConflictException, IOException {
RevCommit parentCommit = original.getParent(0);
@@ -372,8 +445,9 @@
if (!allowConflicts || !(merger instanceof ResolveMerger)) {
throw new MergeConflictException(
- "The change could not be rebased due to a conflict during merge.\n\n"
- + MergeUtil.createConflictMessage(conflicts));
+ String.format(
+ "Change %s could not be rebased due to a conflict during merge.\n\n%s",
+ originalChangeId.toString(), MergeUtil.createConflictMessage(conflicts)));
}
Map<String, MergeResult<? extends Sequence>> mergeResults =
@@ -413,7 +487,6 @@
cb.getAuthor(), cb.getCommitter().getWhen(), cb.getCommitter().getTimeZone()));
}
ObjectId objectId = ctx.getInserter().insert(cb);
- ctx.getInserter().flush();
CodeReviewCommit commit = ((CodeReviewRevWalk) ctx.getRevWalk()).parseCommit(objectId);
commit.setFilesWithGitConflicts(filesWithGitConflicts);
return commit;
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index ba938ee..8acc925 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -22,12 +22,19 @@
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.git.ObjectIds;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Inject;
@@ -46,20 +53,65 @@
private final Provider<InternalChangeQuery> queryProvider;
private final ChangeNotes.Factory notesFactory;
private final PatchSetUtil psUtil;
+ private final RebaseChangeOp.Factory rebaseFactory;
@Inject
RebaseUtil(
Provider<InternalChangeQuery> queryProvider,
ChangeNotes.Factory notesFactory,
- PatchSetUtil psUtil) {
+ PatchSetUtil psUtil,
+ RebaseChangeOp.Factory rebaseFactory) {
this.queryProvider = queryProvider;
this.notesFactory = notesFactory;
this.psUtil = psUtil;
+ this.rebaseFactory = rebaseFactory;
+ }
+
+ /**
+ * Checks whether the given change fulfills all preconditions to be rebased.
+ *
+ * <p>This method does not check whether the calling user is allowed to rebase the change.
+ */
+ public void verifyRebasePreconditions(RevWalk rw, ChangeNotes changeNotes, PatchSet patchSet)
+ throws ResourceConflictException, IOException {
+ // Not allowed to rebase if the current patch set is locked.
+ psUtil.checkPatchSetNotLocked(changeNotes);
+
+ Change change = changeNotes.getChange();
+ if (!change.isNew()) {
+ throw new ResourceConflictException(
+ String.format("Change %s is %s", change.getId(), ChangeUtil.status(change)));
+ }
+
+ if (!hasOneParent(rw, patchSet)) {
+ throw new ResourceConflictException(
+ String.format(
+ "Error rebasing %s. Cannot rebase %s",
+ change.getId(),
+ countParents(rw, patchSet) > 1 ? "merge commits" : "commit with no ancestor"));
+ }
+ }
+
+ public static boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
+ // Prevent rebase of exotic changes (merge commit, no ancestor).
+ return countParents(rw, ps) == 1;
+ }
+
+ private static int countParents(RevWalk rw, PatchSet ps) throws IOException {
+ RevCommit c = rw.parseCommit(ps.commitId());
+ return c.getParentCount();
+ }
+
+ private static boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip) throws IOException {
+ ObjectId baseId = base.commitId();
+ ObjectId tipId = tip.commitId();
+ return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
}
public boolean canRebase(PatchSet patchSet, BranchNameKey dest, Repository git, RevWalk rw) {
try {
- findBaseRevision(patchSet, dest, git, rw);
+ @SuppressWarnings("unused")
+ ObjectId base = findBaseRevision(patchSet, dest, git, rw, true);
return true;
} catch (RestApiException e) {
return false;
@@ -129,6 +181,100 @@
}
/**
+ * Parse or find the commit onto which a patch set should be rebased.
+ *
+ * <p>If a {@code rebaseInput.base} is provided, parse it. Otherwise, finds the latest patch set
+ * of the change corresponding to this commit's parent, or the destination branch tip in the case
+ * where the parent's change is merged.
+ *
+ * @param git the repository.
+ * @param rw the RevWalk.
+ * @param permissionBackend to check base reading permissions with.
+ * @param rsrc to find the base for
+ * @param rebaseInput to optionally parse the base from.
+ * @param verifyNeedsRebase whether to verify if the change base is not already up to date
+ * @return the commit onto which the patch set should be rebased.
+ * @throws RestApiException if rebase is not possible.
+ * @throws IOException if accessing the repository fails.
+ * @throws PermissionBackendException if the user don't have permissions to read the base change.
+ */
+ public ObjectId parseOrFindBaseRevision(
+ Repository git,
+ RevWalk rw,
+ PermissionBackend permissionBackend,
+ RevisionResource rsrc,
+ RebaseInput rebaseInput,
+ boolean verifyNeedsRebase)
+ throws RestApiException, IOException, PermissionBackendException {
+ Change change = rsrc.getChange();
+
+ if (rebaseInput == null || rebaseInput.base == null) {
+ return findBaseRevision(rsrc.getPatchSet(), change.getDest(), git, rw, verifyNeedsRebase);
+ }
+
+ String inputBase = rebaseInput.base.trim();
+
+ if (inputBase.isEmpty()) {
+ return getDestRefTip(git, change.getDest());
+ }
+
+ Base base;
+ try {
+ base = parseBase(rsrc, inputBase);
+ } catch (NoSuchChangeException e) {
+ throw new UnprocessableEntityException(
+ String.format("Base change not found: %s", inputBase), e);
+ }
+ if (base == null) {
+ throw new ResourceConflictException(
+ "base revision is missing from the destination branch: " + inputBase);
+ }
+ return getLatestRevisionForBaseChange(rw, permissionBackend, rsrc, base);
+ }
+
+ private ObjectId getDestRefTip(Repository git, BranchNameKey destRefKey)
+ throws ResourceConflictException, IOException {
+ // Remove existing dependency to other patch set.
+ Ref destRef = git.exactRef(destRefKey.branch());
+ if (destRef == null) {
+ throw new ResourceConflictException(
+ "can't rebase onto tip of branch " + destRefKey.branch() + "; branch doesn't exist");
+ }
+ return destRef.getObjectId();
+ }
+
+ private ObjectId getLatestRevisionForBaseChange(
+ RevWalk rw, PermissionBackend permissionBackend, RevisionResource childRsrc, Base base)
+ throws ResourceConflictException, AuthException, PermissionBackendException, IOException {
+
+ Change child = childRsrc.getChange();
+ PatchSet.Id baseId = base.patchSet().id();
+ if (child.getId().equals(baseId.changeId())) {
+ throw new ResourceConflictException(
+ String.format("cannot rebase change %s onto itself", childRsrc.getChange().getId()));
+ }
+
+ permissionBackend.user(childRsrc.getUser()).change(base.notes()).check(ChangePermission.READ);
+
+ Change baseChange = base.notes().getChange();
+ if (!baseChange.getProject().equals(child.getProject())) {
+ throw new ResourceConflictException(
+ "base change is in wrong project: " + baseChange.getProject());
+ } else if (!baseChange.getDest().equals(child.getDest())) {
+ throw new ResourceConflictException(
+ "base change is targeting wrong branch: " + baseChange.getDest());
+ } else if (baseChange.isAbandoned()) {
+ throw new ResourceConflictException("base change is abandoned: " + baseChange.getKey());
+ } else if (isMergedInto(rw, childRsrc.getPatchSet(), base.patchSet())) {
+ throw new ResourceConflictException(
+ "base change "
+ + baseChange.getKey()
+ + " is a descendant of the current change - recursion not allowed");
+ }
+ return base.patchSet().commitId();
+ }
+
+ /**
* Find the commit onto which a patch set should be rebased.
*
* <p>This is defined as the latest patch set of the change corresponding to this commit's parent,
@@ -138,12 +284,17 @@
* @param destBranch the destination branch.
* @param git the repository.
* @param rw the RevWalk.
+ * @param verifyNeedsRebase whether to verify if the change base is not already up to date
* @return the commit onto which the patch set should be rebased.
* @throws RestApiException if rebase is not possible.
* @throws IOException if accessing the repository fails.
*/
public ObjectId findBaseRevision(
- PatchSet patchSet, BranchNameKey destBranch, Repository git, RevWalk rw)
+ PatchSet patchSet,
+ BranchNameKey destBranch,
+ Repository git,
+ RevWalk rw,
+ boolean verifyNeedsRebase)
throws RestApiException, IOException {
ObjectId baseId = null;
RevCommit commit = rw.parseCommit(patchSet.commitId());
@@ -170,7 +321,7 @@
}
if (depChange.isNew()) {
- if (depPatchSet.id().equals(depChange.currentPatchSetId())) {
+ if (verifyNeedsRebase && depPatchSet.id().equals(depChange.currentPatchSetId())) {
throw new ResourceConflictException(
"Change is already based on the latest patch set of the dependent change.");
}
@@ -189,10 +340,29 @@
"The destination branch does not exist: " + destBranch.branch());
}
baseId = destRef.getObjectId();
- if (baseId.equals(parentId)) {
+ if (verifyNeedsRebase && baseId.equals(parentId)) {
throw new ResourceConflictException("Change is already up to date.");
}
}
return baseId;
}
+
+ public RebaseChangeOp getRebaseOp(RevisionResource revRsrc, RebaseInput input, ObjectId baseRev) {
+ return applyRebaseInputToOp(
+ rebaseFactory.create(revRsrc.getNotes(), revRsrc.getPatchSet(), baseRev), input);
+ }
+
+ public RebaseChangeOp getRebaseOp(
+ RevisionResource revRsrc, RebaseInput input, Change.Id baseChange) {
+ return applyRebaseInputToOp(
+ rebaseFactory.create(revRsrc.getNotes(), revRsrc.getPatchSet(), baseChange), input);
+ }
+
+ private RebaseChangeOp applyRebaseInputToOp(RebaseChangeOp op, RebaseInput input) {
+ return op.setForceContentMerge(true)
+ .setAllowConflicts(input.allowConflicts)
+ .setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions))
+ .setFireRevisionCreated(true);
+ }
}
diff --git a/java/com/google/gerrit/server/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
index b6e3121..f4b1a83c 100644
--- a/java/com/google/gerrit/server/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -75,16 +75,7 @@
checkArgument(!in.isEmpty(), "Input may not be empty");
// Map of all patch sets, keyed by commit SHA-1.
Map<ObjectId, PatchSetData> byId = collectById(in);
- PatchSetData start = byId.get(startPs.commitId());
- requireNonNull(
- start,
- () ->
- String.format(
- "commit %s of patch set %s not found in %s",
- startPs.commitId().name(),
- startPs.id(),
- byId.entrySet().stream()
- .collect(toMap(e -> e.getKey().name(), e -> e.getValue().patchSet().id()))));
+ PatchSetData start = getCheckedPatchSetData(byId, startPs);
// Map of patch set -> immediate parent.
ListMultimap<PatchSetData, PatchSetData> parents =
@@ -120,6 +111,34 @@
return result;
}
+ public List<PatchSetData> sortAncestors(List<ChangeData> in, PatchSet startPs)
+ throws IOException, PermissionBackendException {
+ checkArgument(!in.isEmpty(), "Input may not be empty");
+ // Map of all patch sets, keyed by commit SHA-1.
+ Map<ObjectId, PatchSetData> byId = collectById(in);
+ PatchSetData start = getCheckedPatchSetData(byId, startPs);
+
+ // Map of patch set -> immediate parent.
+ ListMultimap<PatchSetData, PatchSetData> parents =
+ MultimapBuilder.hashKeys(in.size()).arrayListValues(3).build();
+
+ for (ChangeData cd : in) {
+ for (PatchSet ps : cd.patchSets()) {
+ PatchSetData thisPsd = requireNonNull(byId.get(ps.commitId()));
+
+ for (RevCommit p : thisPsd.commit().getParents()) {
+ PatchSetData parentPsd = byId.get(p);
+ if (parentPsd != null) {
+ parents.put(thisPsd, parentPsd);
+ }
+ }
+ }
+ }
+
+ Collection<PatchSetData> ancestors = walkAncestors(parents, start);
+ return List.copyOf(ancestors);
+ }
+
private Map<ObjectId, PatchSetData> collectById(List<ChangeData> in) throws IOException {
Project.NameKey project = in.get(0).change().getProject();
Map<ObjectId, PatchSetData> result = Maps.newHashMapWithExpectedSize(in.size() * 3);
@@ -143,6 +162,19 @@
return result;
}
+ private PatchSetData getCheckedPatchSetData(Map<ObjectId, PatchSetData> byId, PatchSet ps) {
+ PatchSetData psData = byId.get(ps.commitId());
+ return requireNonNull(
+ psData,
+ () ->
+ String.format(
+ "commit %s of patch set %s not found in %s",
+ ps.commitId().name(),
+ ps.id(),
+ byId.entrySet().stream()
+ .collect(toMap(e -> e.getKey().name(), e -> e.getValue().patchSet().id()))));
+ }
+
private Collection<PatchSetData> walkAncestors(
ListMultimap<PatchSetData, PatchSetData> parents, PatchSetData start)
throws PermissionBackendException {
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index f3ad4f7..b5e0181 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -277,8 +277,9 @@
IdentifiedUser reviewerUser;
boolean exactMatchFound = false;
try {
- if (input instanceof InternalReviewerInput
- && ((InternalReviewerInput) input).skipVisibilityCheck) {
+ if (ReviewerState.REMOVED.equals(input.state)
+ || (input instanceof InternalReviewerInput
+ && ((InternalReviewerInput) input).skipVisibilityCheck)) {
reviewerUser =
accountResolver.resolveIncludeInactiveIgnoreVisibility(input.reviewer).asUniqueUser();
} else {
diff --git a/java/com/google/gerrit/server/change/ValidationOptionsUtil.java b/java/com/google/gerrit/server/change/ValidationOptionsUtil.java
new file mode 100644
index 0000000..137239c
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ValidationOptionsUtil.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2022 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.change;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.common.Nullable;
+import java.util.Map;
+
+/** Utilities for validation options parsing. */
+public final class ValidationOptionsUtil {
+ public static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
+ @Nullable Map<String, String> validationOptions) {
+ if (validationOptions == null) {
+ return ImmutableListMultimap.of();
+ }
+
+ ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
+ ImmutableListMultimap.builder();
+ validationOptions
+ .entrySet()
+ .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
+ return validationOptionsBuilder.build();
+ }
+
+ private ValidationOptionsUtil() {}
+}
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index 5a4580c..32ec401 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -20,14 +20,9 @@
public class ExperimentFeaturesConstants {
/** Features that are known experiments and can be referenced in the code. */
- public static String UI_FEATURE_PATCHSET_COMMENTS = "UiFeature__patchset_comments";
-
- public static String UI_FEATURE_SUBMIT_REQUIREMENTS_UI = "UiFeature__submit_requirements_ui";
-
public static String GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION =
"GerritBackendFeature__attach_nonce_to_documentation";
/** Features, enabled by default in the current release. */
- public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES =
- ImmutableSet.of(UI_FEATURE_PATCHSET_COMMENTS, UI_FEATURE_SUBMIT_REQUIREMENTS_UI);
+ public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES = ImmutableSet.of();
}
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 2841f92..f0b2a78 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -17,7 +17,6 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -41,6 +40,7 @@
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.NotifyResolver;
+import com.google.gerrit.server.change.ValidationOptionsUtil;
import com.google.gerrit.server.extensions.events.ChangeReverted;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.RevertedSender;
@@ -64,7 +64,6 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
@@ -317,7 +316,8 @@
.create(changeId, revertCommit, changeToRevert.getDest().branch())
.setTopic(input.topic == null ? changeToRevert.getTopic() : input.topic.trim());
ins.setMessage("Uploaded patch set 1.");
- ins.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
+ ins.setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
@@ -344,20 +344,6 @@
return changeId;
}
- private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
- @Nullable Map<String, String> validationOptions) {
- if (validationOptions == null) {
- return ImmutableListMultimap.of();
- }
-
- ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
- ImmutableListMultimap.builder();
- validationOptions
- .entrySet()
- .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
- return validationOptionsBuilder.build();
- }
-
/**
* Notify the owners of a change that their change is being reverted.
*
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index c0c934b..87d8db1 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -115,7 +115,6 @@
private final RetryHelper retryHelper;
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@@ -150,7 +149,6 @@
}
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@@ -185,7 +183,6 @@
Optional.of(currentUser));
}
- @SuppressWarnings("BindingAnnotationWithoutInject")
private GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index 213094e..352d376 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -116,9 +116,9 @@
*/
public static Set<String> projectFields(QueryOptions opts) {
Set<String> fs = opts.fields();
- return fs.contains(ProjectField.NAME.getName())
+ return fs.contains(ProjectField.NAME_SPEC.getName())
? fs
- : Sets.union(fs, ImmutableSet.of(ProjectField.NAME.getName()));
+ : Sets.union(fs, ImmutableSet.of(ProjectField.NAME_SPEC.getName()));
}
private IndexUtils() {
diff --git a/java/com/google/gerrit/server/index/account/AccountIndex.java b/java/com/google/gerrit/server/index/account/AccountIndex.java
index ca7264c..66b85af 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndex.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -20,6 +20,7 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.query.account.AccountPredicates;
+import java.util.function.Function;
/**
* Index for Gerrit accounts. This class is mainly used for typing the generic parent class that
@@ -32,4 +33,6 @@
default Predicate<AccountState> keyPredicate(Account.Id id) {
return AccountPredicates.id(getSchema(), id);
}
+
+ Function<AccountState, Account.Id> ENTITY_TO_KEY = (a) -> a.account().id();
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 254a7e5..1c89566f 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -988,7 +988,7 @@
/** Serialized change object, used for pre-populating results. */
private static final TypeToken<Entities.Change> CHANGE_TYPE_TOKEN =
- new TypeToken<Entities.Change>() {
+ new TypeToken<>() {
private static final long serialVersionUID = 1L;
};
@@ -1007,7 +1007,7 @@
/** Serialized approvals for the current patch set, used for pre-populating results. */
private static final TypeToken<Iterable<Entities.PatchSetApproval>> APPROVAL_TYPE_TOKEN =
- new TypeToken<Iterable<Entities.PatchSetApproval>>() {
+ new TypeToken<>() {
private static final long serialVersionUID = 1L;
};
@@ -1086,7 +1086,7 @@
public static final IndexedField<ChangeData, String>.SearchSpec COMMIT_MESSAGE_EXACT =
COMMIT_MESSAGE_EXACT_FIELD.exact(ChangeQueryBuilder.FIELD_MESSAGE_EXACT);
- /** Commit message of the current patch set. */
+ /** Subject of the current patch set (aka first line of the commit message). */
public static final IndexedField<ChangeData, String> SUBJECT_FIELD =
IndexedField.<ChangeData>stringBuilder("Subject")
.required()
@@ -1095,6 +1095,9 @@
public static final IndexedField<ChangeData, String>.SearchSpec SUBJECT_SPEC =
SUBJECT_FIELD.fullText(ChangeQueryBuilder.FIELD_SUBJECT);
+ public static final IndexedField<ChangeData, String>.SearchSpec PREFIX_SUBJECT_SPEC =
+ SUBJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PREFIX_SUBJECT);
+
/** Summary or inline comment. */
public static final IndexedField<ChangeData, Iterable<String>> COMMENT_FIELD =
IndexedField.<ChangeData>iterableStringBuilder("Comment")
@@ -1297,7 +1300,7 @@
/** Serialized patch set object, used for pre-populating results. */
private static final TypeToken<Iterable<Entities.PatchSet>> PATCH_SET_TYPE_TOKEN =
- new TypeToken<Iterable<Entities.PatchSet>>() {
+ new TypeToken<>() {
private static final long serialVersionUID = 1L;
};
@@ -1618,7 +1621,7 @@
/** Serialized submit requirements, used for pre-populating results. */
private static final TypeToken<Iterable<Cache.SubmitRequirementResultProto>>
STORED_SUBMIT_REQUIREMENTS_TYPE_TOKEN =
- new TypeToken<Iterable<Cache.SubmitRequirementResultProto>>() {
+ new TypeToken<>() {
private static final long serialVersionUID = 1L;
};
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 6fc2665..74e9af1 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -20,6 +20,7 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangePredicates;
+import java.util.function.Function;
/**
* Index for Gerrit changes. This class is mainly used for typing the generic parent class that
@@ -32,4 +33,6 @@
default Predicate<ChangeData> keyPredicate(Change.Id id) {
return ChangePredicates.idStr(id);
}
+
+ Function<ChangeData, Change.Id> ENTITY_TO_KEY = ChangeData::getId;
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 4b88919..dc3907d 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -46,6 +46,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
@@ -283,9 +284,9 @@
* @param id change to delete.
* @return future for the deleting task, the result of the future is always {@code null}
*/
- public ListenableFuture<ChangeData> deleteAsync(Change.Id id) {
+ public ListenableFuture<ChangeData> deleteAsync(Project.NameKey project, Change.Id id) {
fireChangeScheduledForDeletionFromIndexEvent(id.get());
- return submit(new DeleteTask(id));
+ return submit(new DeleteTask(id, Optional.of(project)));
}
/**
@@ -298,8 +299,12 @@
doDelete(id);
}
+ private void doDelete(Project.NameKey project, Change.Id id) {
+ new DeleteTask(id, Optional.of(project)).call();
+ }
+
private void doDelete(Change.Id id) {
- new DeleteTask(id).call();
+ new DeleteTask(id, Optional.empty()).call();
}
/**
@@ -423,7 +428,7 @@
doIndex(changeData);
return changeData;
} catch (NoSuchChangeException e) {
- doDelete(id);
+ doDelete(project, id);
}
return null;
}
@@ -456,9 +461,11 @@
// Not AbstractIndexTask as it doesn't need a request context.
private class DeleteTask implements Callable<ChangeData> {
private final Change.Id id;
+ private final Optional<Project.NameKey> project;
- private DeleteTask(Change.Id id) {
+ private DeleteTask(Change.Id id, Optional<Project.NameKey> project) {
this.id = id;
+ this.project = project;
}
@Nullable
@@ -476,7 +483,12 @@
.changeId(id.get())
.indexVersion(i.getSchema().getVersion())
.build())) {
- i.delete(id);
+ // Some index implementation require ProjectKey to build a database key
+ // If delete(K) method is used, this will require changeId -> projectKey lookup (index
+ // query), which is expensive.
+ // Use changeData with ProjectKey and deleteByValue(V) method, if possible
+ project.ifPresentOrElse(
+ p -> i.deleteByValue(changeDataFactory.create(p, id)), () -> i.delete(id));
} catch (RuntimeException e) {
throw new StorageException(
String.format(
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 5dc1500..895c4d8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -214,6 +214,7 @@
@Deprecated static final Schema<ChangeData> V78 = schema(V77);
/** Remove draft and star fields. */
+ @Deprecated
static final Schema<ChangeData> V79 =
new Schema.Builder<ChangeData>()
.add(V78)
@@ -222,6 +223,7 @@
.build();
/** Add subject field. */
+ @Deprecated
static final Schema<ChangeData> V80 =
new Schema.Builder<ChangeData>()
.add(V79)
@@ -229,6 +231,13 @@
.addSearchSpecs(ChangeField.SUBJECT_SPEC)
.build();
+ /** Add prefixsubject field. */
+ static final Schema<ChangeData> V81 =
+ new Schema.Builder<ChangeData>()
+ .add(V80)
+ .addSearchSpecs(ChangeField.PREFIX_SUBJECT_SPEC)
+ .build();
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 26477a4..8f5e36e 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.index.change.ChangeField.CHANGE_SPEC;
+import static com.google.gerrit.server.index.change.ChangeField.NUMERIC_ID_STR_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC;
import com.google.common.annotations.VisibleForTesting;
@@ -68,10 +69,13 @@
int pageSizeMultiplier,
int limit,
Set<String> fields) {
- // Always include project since it is needed to load the change from NoteDb.
- if (!fields.contains(CHANGE_SPEC.getName()) && !fields.contains(PROJECT_SPEC.getName())) {
+ // Always include project and change id since both are needed to load the change from NoteDb.
+ if (!fields.contains(CHANGE_SPEC.getName())
+ && !(fields.contains(PROJECT_SPEC.getName())
+ && fields.contains(NUMERIC_ID_STR_SPEC.getName()))) {
fields = new HashSet<>(fields);
fields.add(PROJECT_SPEC.getName());
+ fields.add(NUMERIC_ID_STR_SPEC.getName());
}
return QueryOptions.create(config, start, pageSize, pageSizeMultiplier, limit, fields);
}
diff --git a/java/com/google/gerrit/server/index/group/GroupIndex.java b/java/com/google/gerrit/server/index/group/GroupIndex.java
index 28c0384..f6a9224 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndex.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndex.java
@@ -20,6 +20,7 @@
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.query.group.GroupPredicates;
+import java.util.function.Function;
/**
* Index for internal Gerrit groups. This class is mainly used for typing the generic parent class
@@ -33,4 +34,6 @@
default Predicate<InternalGroup> keyPredicate(AccountGroup.UUID uuid) {
return GroupPredicates.uuid(uuid);
}
+
+ Function<InternalGroup, AccountGroup.UUID> ENTITY_TO_KEY = (g) -> g.getGroupUUID();
}
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java
index 9c44c00..9f6bb31 100644
--- a/java/com/google/gerrit/server/index/project/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -40,7 +40,7 @@
*/
public class StalenessChecker {
private static final ImmutableSet<String> FIELDS =
- ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
+ ImmutableSet.of(ProjectField.NAME_SPEC.getName(), ProjectField.REF_STATE.getName());
private final ProjectCache projectCache;
private final ProjectIndexCollection indexes;
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index e362c4b..2b8a501 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -85,7 +85,13 @@
import java.util.Optional;
import java.util.Set;
-/** A service that can attach the comments from a {@link MailMessage} to a change. */
+/**
+ * Users can post comments on gerrit changes by replying directly to gerrit emails. This service
+ * parses the {@link MailMessage} sent by users and attaches the comments to a change.
+ *
+ * <p>This functionality can be configured or disabled by host. See {@link
+ * com.google.gerrit.server.mail.receive.MailReceiver.MailReceiverModule}
+ */
@Singleton
public class MailProcessor {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/mail/receive/MailReceiver.java b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
index 23e1cc3..a308168 100644
--- a/java/com/google/gerrit/server/mail/receive/MailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
@@ -131,27 +131,20 @@
if (async) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
- workQueue
- .getDefaultQueue()
- .submit(
- () -> {
- try {
- mailProcessor.process(m);
- requestDeletion(m.id());
- } catch (RestApiException | UpdateException e) {
- logger.atSevere().withCause(e).log(
- "Mail: Can't process message %s . Won't delete.", m.id());
- }
- });
+ workQueue.getDefaultQueue().submit(() -> processMessage(m));
} else {
// Synchronous processing is used only in tests.
- try {
- mailProcessor.process(m);
- requestDeletion(m.id());
- } catch (RestApiException | UpdateException e) {
- logger.atSevere().withCause(e).log("Mail: Can't process messages. Won't delete.");
- }
+ processMessage(m);
}
}
}
+
+ private void processMessage(MailMessage m) {
+ try {
+ mailProcessor.process(m);
+ requestDeletion(m.id());
+ } catch (RestApiException | UpdateException e) {
+ logger.atSevere().withCause(e).log("Mail: Can't process message %s . Won't delete.", m.id());
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index 800066e..968bb1a 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -98,7 +98,7 @@
}
@Nullable
- public List<String> getReviewerNames() {
+ private List<String> getReviewerNames() {
if (reviewers.isEmpty()) {
return null;
}
@@ -110,7 +110,7 @@
}
@Nullable
- public List<String> getRemovedReviewerNames() {
+ private List<String> getRemovedReviewerNames() {
if (removedReviewers.isEmpty() && removedByEmailReviewers.isEmpty()) {
return null;
}
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 9299d74..cbf47c5 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -16,6 +16,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -24,6 +25,7 @@
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.NotifyConfig;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
@@ -35,11 +37,13 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.GroupBackedUser;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
public class ProjectWatch {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -240,15 +244,16 @@
}
private boolean filterMatch(CurrentUser user, String filter) throws QueryParseException {
- ChangeQueryBuilder qb;
+ WatcherChangeQueryBuilder qb;
Predicate<ChangeData> p = null;
if (user == null) {
- qb = args.queryBuilder.get().asUser(args.anonymousUser.get());
+ qb = WatcherChangeQueryBuilder.asUser(args.queryBuilder.get(), args.anonymousUser.get());
} else {
- qb = args.queryBuilder.get().asUser(user);
+ qb = WatcherChangeQueryBuilder.asUser(args.queryBuilder.get(), user);
p = qb.isVisible();
}
+ qb.forceAccountVisibilityCheck();
if (filter != null) {
Predicate<ChangeData> filterPredicate = qb.parse(filter);
@@ -260,4 +265,40 @@
}
return p == null || p.asMatchable().match(changeData);
}
+
+ private static class WatcherChangeQueryBuilder extends ChangeQueryBuilder {
+ private WatcherChangeQueryBuilder(Arguments args) {
+ super(args);
+ }
+
+ public static WatcherChangeQueryBuilder asUser(ChangeQueryBuilder other, CurrentUser user) {
+ return new WatcherChangeQueryBuilder(other.getArgs().asUser(user));
+ }
+
+ @Override
+ protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
+ if (query.startsWith("refs/")) {
+ return ref(query);
+ }
+
+ // Adapt the capacity of this list when adding more default predicates.
+ List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11);
+ predicates.add(file(query));
+ try {
+ predicates.add(label(query));
+ } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
+ // Skip.
+ }
+ predicates.add(commit(query));
+ predicates.add(message(query));
+ predicates.add(comment(query));
+ predicates.add(projects(query));
+ predicates.add(ref(query));
+ predicates.add(branch(query));
+ predicates.add(topic(query));
+ // Adapt the capacity of the "predicates" list when adding more default
+ // predicates.
+ return Predicate.or(predicates);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
index 2b856fb..43a212c 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -28,6 +28,7 @@
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
@@ -66,6 +67,7 @@
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.HistogramDiff;
import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -76,6 +78,7 @@
/** Implementation of the {@link GitFileDiffCache} */
@Singleton
public class GitFileDiffCacheImpl implements GitFileDiffCache {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String GIT_DIFF = "git_file_diff";
public static Module module() {
@@ -340,8 +343,7 @@
throws IOException {
if (!key.useTimeout()) {
try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
- FileHeader fileHeader = formatter.get().toFileHeader(diffEntry);
- return GitFileDiff.create(diffEntry, fileHeader);
+ return GitFileDiff.create(diffEntry, getFileHeader(formatter, diffEntry));
}
}
// This submits the DiffFormatter to a different thread. The CloseablePool and our usage of it
@@ -353,7 +355,7 @@
diffExecutor.submit(
() -> {
try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
- return GitFileDiff.create(diffEntry, formatter.get().toFileHeader(diffEntry));
+ return GitFileDiff.create(diffEntry, getFileHeader(formatter, diffEntry));
}
});
try {
@@ -385,6 +387,46 @@
? diffEntry.getOldPath()
: diffEntry.getNewPath();
}
+
+ private FileHeader getFileHeader(
+ CloseablePool<DiffFormatter>.Handle formatter, DiffEntry diffEntry) throws IOException {
+ logger.atFine().log("getting file header for %s", formatDiffEntryForLogging(diffEntry));
+ try {
+ return formatter.get().toFileHeader(diffEntry);
+ } catch (MissingObjectException e) {
+ throw new IOException(
+ String.format("Failed to get file header for %s", formatDiffEntryForLogging(diffEntry)),
+ e);
+ }
+ }
+
+ private String formatDiffEntryForLogging(DiffEntry diffEntry) {
+ StringBuilder buf = new StringBuilder();
+ buf.append("DiffEntry[");
+ buf.append(diffEntry.getChangeType());
+ buf.append(" ");
+ switch (diffEntry.getChangeType()) {
+ case ADD:
+ buf.append(String.format("%s (%s)", diffEntry.getNewPath(), diffEntry.getNewId().name()));
+ break;
+ case COPY:
+ case RENAME:
+ buf.append(
+ String.format(
+ "%s (%s) -> %s (%s)",
+ diffEntry.getOldPath(),
+ diffEntry.getOldId().name(),
+ diffEntry.getNewPath(),
+ diffEntry.getNewId().name()));
+ break;
+ case DELETE:
+ case MODIFY:
+ buf.append(String.format("%s (%s)", diffEntry.getOldPath(), diffEntry.getOldId().name()));
+ break;
+ }
+ buf.append("]");
+ return buf.toString();
+ }
}
/**
diff --git a/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java b/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
new file mode 100644
index 0000000..622f0cf
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2022 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.permissions;
+
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.server.util.LabelVote;
+
+/** Abstract permission representing a label. */
+public abstract class AbstractLabelPermission implements ChangePermissionOrLabel {
+ public enum ForUser {
+ SELF,
+ ON_BEHALF_OF
+ }
+
+ protected final ForUser forUser;
+ protected final String name;
+
+ /**
+ * Construct a reference to an abstract label permission.
+ *
+ * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+ * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+ */
+ public AbstractLabelPermission(ForUser forUser, String name) {
+ this.forUser = requireNonNull(forUser, "ForUser");
+ this.name = LabelType.checkName(name);
+ }
+
+ /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
+ public ForUser forUser() {
+ return forUser;
+ }
+
+ /** Returns name of the label, e.g. {@code "Code-Review"}. */
+ public String label() {
+ return name;
+ }
+
+ protected abstract String permissionPrefix();
+
+ protected String permissionName() {
+ if (forUser == ON_BEHALF_OF) {
+ return permissionPrefix() + "As";
+ }
+ return permissionPrefix();
+ }
+
+ @Override
+ public final String describeForException() {
+ if (forUser == ON_BEHALF_OF) {
+ return permissionPrefix() + " on behalf of " + name;
+ }
+ return permissionPrefix() + " " + name;
+ }
+
+ @Override
+ public final int hashCode() {
+ return (permissionPrefix() + name).hashCode();
+ }
+
+ @Override
+ @SuppressWarnings("EqualsGetClass")
+ public final boolean equals(Object other) {
+ if (this.getClass().isAssignableFrom(other.getClass())) {
+ AbstractLabelPermission b = (AbstractLabelPermission) other;
+ return forUser == b.forUser && name.equals(b.name);
+ }
+ return false;
+ }
+
+ @Override
+ public final String toString() {
+ return permissionName() + "[" + name + ']';
+ }
+
+ /** A {@link AbstractLabelPermission} at a specific value. */
+ public abstract static class WithValue implements ChangePermissionOrLabel {
+ private final ForUser forUser;
+ private final LabelVote label;
+
+ /**
+ * Construct a reference to an abstract label permission at a specific value.
+ *
+ * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+ * @param label label name and vote.
+ */
+ public WithValue(ForUser forUser, LabelVote label) {
+ this.forUser = requireNonNull(forUser, "ForUser");
+ this.label = requireNonNull(label, "LabelVote");
+ }
+
+ /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
+ public ForUser forUser() {
+ return forUser;
+ }
+
+ /** Returns name of the label, e.g. {@code "Code-Review"}. */
+ public String label() {
+ return label.label();
+ }
+
+ /** Returns specific value of the label, e.g. 1 or 2. */
+ public short value() {
+ return label.value();
+ }
+
+ public abstract String permissionName();
+
+ @Override
+ public final String describeForException() {
+ if (forUser == ON_BEHALF_OF) {
+ return permissionName() + " on behalf of " + label.formatWithEquals();
+ }
+ return permissionName() + " " + label.formatWithEquals();
+ }
+
+ @Override
+ public final int hashCode() {
+ return (permissionName() + label).hashCode();
+ }
+
+ @Override
+ @SuppressWarnings("EqualsGetClass")
+ public final boolean equals(Object other) {
+ if (this.getClass().isAssignableFrom(other.getClass())) {
+ AbstractLabelPermission.WithValue b = (AbstractLabelPermission.WithValue) other;
+ return forUser == b.forUser && label.equals(b.label);
+ }
+ return false;
+ }
+
+ @Override
+ public final String toString() {
+ if (forUser == ON_BEHALF_OF) {
+ return permissionName() + "As[" + label.format() + ']';
+ }
+ return permissionName() + "[" + label.format() + ']';
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 8d432c8..e36ce7b 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.permissions;
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
import static com.google.gerrit.server.permissions.DefaultPermissionMappings.labelPermissionName;
-import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -216,7 +216,10 @@
public void check(ChangePermissionOrLabel perm)
throws AuthException, PermissionBackendException {
if (!can(perm)) {
- throw new AuthException(perm.describeForException() + " not permitted");
+ throw new AuthException(
+ perm.describeForException()
+ + " not permitted"
+ + perm.hintForException().map(hint -> " (" + hint + ")").orElse(""));
}
}
@@ -240,10 +243,10 @@
private boolean can(ChangePermissionOrLabel perm) throws PermissionBackendException {
if (perm instanceof ChangePermission) {
return can((ChangePermission) perm);
- } else if (perm instanceof LabelPermission) {
- return can((LabelPermission) perm);
- } else if (perm instanceof LabelPermission.WithValue) {
- return can((LabelPermission.WithValue) perm);
+ } else if (perm instanceof AbstractLabelPermission) {
+ return can((AbstractLabelPermission) perm);
+ } else if (perm instanceof AbstractLabelPermission.WithValue) {
+ return can((AbstractLabelPermission.WithValue) perm);
}
throw new PermissionBackendException(perm + " unsupported");
}
@@ -288,11 +291,11 @@
throw new PermissionBackendException(perm + " unsupported");
}
- private boolean can(LabelPermission perm) {
+ private boolean can(AbstractLabelPermission perm) {
return !label(labelPermissionName(perm)).isEmpty();
}
- private boolean can(LabelPermission.WithValue perm) {
+ private boolean can(AbstractLabelPermission.WithValue perm) {
PermissionRange r = label(labelPermissionName(perm));
if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) {
return false;
diff --git a/java/com/google/gerrit/server/permissions/ChangePermission.java b/java/com/google/gerrit/server/permissions/ChangePermission.java
index 63b0378..6ceed3e 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermission.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -16,7 +16,9 @@
import static java.util.Objects.requireNonNull;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.access.GerritPermission;
+import java.util.Optional;
public enum ChangePermission implements ChangePermissionOrLabel {
READ,
@@ -53,24 +55,40 @@
* <p>Before checking this permission, the caller should first verify the current patch set of the
* change is not locked by calling {@code PatchSetUtil.isPatchSetLocked}.
*/
- REBASE,
+ REBASE(
+ /* description= */ null,
+ /* hint= */ "change owners and users with the 'Submit' or 'Rebase' permission can rebase"
+ + " if they have the 'Push' permission"),
REVERT,
SUBMIT,
SUBMIT_AS("submit on behalf of other users"),
TOGGLE_WORK_IN_PROGRESS_STATE;
private final String description;
+ private final String hint;
ChangePermission() {
this.description = null;
+ this.hint = null;
}
ChangePermission(String description) {
this.description = requireNonNull(description);
+ this.hint = null;
+ }
+
+ ChangePermission(@Nullable String description, String hint) {
+ this.description = description;
+ this.hint = requireNonNull(hint);
}
@Override
public String describeForException() {
return description != null ? description : GerritPermission.describeEnumValue(this);
}
+
+ @Override
+ public Optional<String> hintForException() {
+ return Optional.ofNullable(hint);
+ }
}
diff --git a/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java b/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
index 2824efd..9254158 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
@@ -15,6 +15,17 @@
package com.google.gerrit.server.permissions;
import com.google.gerrit.extensions.api.access.GerritPermission;
+import java.util.Optional;
-/** A {@link ChangePermission} or a {@link LabelPermission}. */
-public interface ChangePermissionOrLabel extends GerritPermission {}
+/** A {@link ChangePermission} or a {@link AbstractLabelPermission}. */
+public interface ChangePermissionOrLabel extends GerritPermission {
+ /**
+ * A hint that explains under which conditions this permission is permitted.
+ *
+ * <p>This is useful for permissions that are not directly assigned but are indirectly permitted
+ * by the user having other permissions or being the change owner.
+ */
+ default Optional<String> hintForException() {
+ return Optional.empty();
+ }
+}
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
index 9d69d9b..89f0493 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -24,7 +24,7 @@
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
import com.google.gerrit.extensions.api.access.PluginPermission;
import com.google.gerrit.extensions.api.access.PluginProjectPermission;
-import com.google.gerrit.server.permissions.LabelPermission.ForUser;
+import com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
@@ -160,19 +160,29 @@
return Optional.ofNullable(CHANGE_PERMISSIONS.inverse().get(permissionName));
}
- public static String labelPermissionName(LabelPermission labelPermission) {
- if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
- return Permission.forLabelAs(labelPermission.label());
+ public static String labelPermissionName(AbstractLabelPermission labelPermission) {
+ if (labelPermission instanceof LabelPermission) {
+ if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
+ return Permission.forLabelAs(labelPermission.label());
+ }
+ return Permission.forLabel(labelPermission.label());
+ } else if (labelPermission instanceof LabelRemovalPermission) {
+ return Permission.forRemoveLabel(labelPermission.label());
}
- return Permission.forLabel(labelPermission.label());
+ throw new IllegalStateException("invalid AbstractLabelPermission subtype");
}
// TODO(dborowitz): Can these share a common superinterface?
- public static String labelPermissionName(LabelPermission.WithValue labelPermission) {
- if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
- return Permission.forLabelAs(labelPermission.label());
+ public static String labelPermissionName(AbstractLabelPermission.WithValue labelPermission) {
+ if (labelPermission instanceof LabelPermission.WithValue) {
+ if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
+ return Permission.forLabelAs(labelPermission.label());
+ }
+ return Permission.forLabel(labelPermission.label());
+ } else if (labelPermission instanceof LabelRemovalPermission.WithValue) {
+ return Permission.forRemoveLabel(labelPermission.label());
}
- return Permission.forLabel(labelPermission.label());
+ throw new IllegalStateException("invalid AbstractLabelPermission.WithValue subtype");
}
private DefaultPermissionMappings() {}
diff --git a/java/com/google/gerrit/server/permissions/LabelPermission.java b/java/com/google/gerrit/server/permissions/LabelPermission.java
index c266caa..4652364 100644
--- a/java/com/google/gerrit/server/permissions/LabelPermission.java
+++ b/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -14,24 +14,14 @@
package com.google.gerrit.server.permissions;
-import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
-import static com.google.gerrit.server.permissions.LabelPermission.ForUser.SELF;
-import static java.util.Objects.requireNonNull;
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.SELF;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.server.util.LabelVote;
/** Permission representing a label. */
-public class LabelPermission implements ChangePermissionOrLabel {
- public enum ForUser {
- SELF,
- ON_BEHALF_OF;
- }
-
- private final ForUser forUser;
- private final String name;
-
+public class LabelPermission extends AbstractLabelPermission {
/**
* Construct a reference to a label permission.
*
@@ -67,55 +57,16 @@
* @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
*/
public LabelPermission(ForUser forUser, String name) {
- this.forUser = requireNonNull(forUser, "ForUser");
- this.name = LabelType.checkName(name);
- }
-
- /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
- public ForUser forUser() {
- return forUser;
- }
-
- /** Returns name of the label, e.g. {@code "Code-Review"}. */
- public String label() {
- return name;
+ super(forUser, name);
}
@Override
- public String describeForException() {
- if (forUser == ON_BEHALF_OF) {
- return "label on behalf of " + name;
- }
- return "label " + name;
- }
-
- @Override
- public int hashCode() {
- return name.hashCode();
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof LabelPermission) {
- LabelPermission b = (LabelPermission) other;
- return forUser == b.forUser && name.equals(b.name);
- }
- return false;
- }
-
- @Override
- public String toString() {
- if (forUser == ON_BEHALF_OF) {
- return "LabelAs[" + name + ']';
- }
- return "Label[" + name + ']';
+ public String permissionPrefix() {
+ return "label";
}
/** A {@link LabelPermission} at a specific value. */
- public static class WithValue implements ChangePermissionOrLabel {
- private final ForUser forUser;
- private final LabelVote label;
-
+ public static class WithValue extends AbstractLabelPermission.WithValue {
/**
* Construct a reference to a label at a specific value.
*
@@ -195,53 +146,12 @@
* @param label label name and vote.
*/
public WithValue(ForUser forUser, LabelVote label) {
- this.forUser = requireNonNull(forUser, "ForUser");
- this.label = requireNonNull(label, "LabelVote");
- }
-
- /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
- public ForUser forUser() {
- return forUser;
- }
-
- /** Returns name of the label, e.g. {@code "Code-Review"}. */
- public String label() {
- return label.label();
- }
-
- /** Returns specific value of the label, e.g. 1 or 2. */
- public short value() {
- return label.value();
+ super(forUser, label);
}
@Override
- public String describeForException() {
- if (forUser == ON_BEHALF_OF) {
- return "label on behalf of " + label.formatWithEquals();
- }
- return "label " + label.formatWithEquals();
- }
-
- @Override
- public int hashCode() {
- return label.hashCode();
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof WithValue) {
- WithValue b = (WithValue) other;
- return forUser == b.forUser && label.equals(b.label);
- }
- return false;
- }
-
- @Override
- public String toString() {
- if (forUser == ON_BEHALF_OF) {
- return "LabelAs[" + label.format() + ']';
- }
- return "Label[" + label.format() + ']';
+ public String permissionName() {
+ return "label";
}
}
}
diff --git a/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java b/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java
new file mode 100644
index 0000000..2553601
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2022 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.permissions;
+
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.SELF;
+
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.server.util.LabelVote;
+
+/** Permission representing a label removal. */
+public class LabelRemovalPermission extends AbstractLabelPermission {
+ /**
+ * Construct a reference to a label removal permission.
+ *
+ * @param type type description of the label.
+ */
+ public LabelRemovalPermission(LabelType type) {
+ this(type.getName());
+ }
+
+ /**
+ * Construct a reference to a label removal permission.
+ *
+ * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+ */
+ public LabelRemovalPermission(String name) {
+ super(SELF, name);
+ }
+
+ @Override
+ public String permissionPrefix() {
+ return "removeLabel";
+ }
+
+ /** A {@link LabelRemovalPermission} at a specific value. */
+ public static class WithValue extends AbstractLabelPermission.WithValue {
+ /**
+ * Construct a reference to a label removal at a specific value.
+ *
+ * @param type description of the label.
+ * @param value numeric score assigned to the label.
+ */
+ public WithValue(LabelType type, LabelValue value) {
+ this(type.getName(), value.getValue());
+ }
+
+ /**
+ * Construct a reference to a label removal at a specific value.
+ *
+ * @param type description of the label.
+ * @param value numeric score assigned to the label.
+ */
+ public WithValue(LabelType type, short value) {
+ this(type.getName(), value);
+ }
+
+ /**
+ * Construct a reference to a label removal at a specific value.
+ *
+ * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+ * @param value numeric score assigned to the label.
+ */
+ public WithValue(String name, short value) {
+ this(LabelVote.create(name, value));
+ }
+
+ /**
+ * Construct a reference to a label removal at a specific value.
+ *
+ * @param label label name and vote.
+ */
+ public WithValue(LabelVote label) {
+ super(SELF, label);
+ }
+
+ @Override
+ public String permissionName() {
+ return "removeLabel";
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index fea2827..eb5e053 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -474,6 +474,18 @@
}
/**
+ * Test which values of a label the user may be able to remove.
+ *
+ * @param label definition of the label to test values of.
+ * @return set containing values the user may be able to use; may be empty if none.
+ * @throws PermissionBackendException if failure consulting backend configuration.
+ */
+ public Set<LabelRemovalPermission.WithValue> testRemoval(LabelType label)
+ throws PermissionBackendException {
+ return test(removalValuesOf(requireNonNull(label, "LabelType")));
+ }
+
+ /**
* Test which values of a group of labels the user may be able to set.
*
* @param types definition of the labels to test values of.
@@ -486,10 +498,29 @@
return test(types.stream().flatMap(t -> valuesOf(t).stream()).collect(toSet()));
}
+ /**
+ * Test which values of a group of labels the user may be able to remove.
+ *
+ * @param types definition of the labels to test values of.
+ * @return set containing values the user may be able to use; may be empty if none.
+ * @throws PermissionBackendException if failure consulting backend configuration.
+ */
+ public Set<LabelRemovalPermission.WithValue> testLabelRemovals(Collection<LabelType> types)
+ throws PermissionBackendException {
+ requireNonNull(types, "LabelType");
+ return test(types.stream().flatMap(t -> removalValuesOf(t).stream()).collect(toSet()));
+ }
+
private static Set<LabelPermission.WithValue> valuesOf(LabelType label) {
return label.getValues().stream()
.map(v -> new LabelPermission.WithValue(label, v))
.collect(toSet());
}
+
+ private static Set<LabelRemovalPermission.WithValue> removalValuesOf(LabelType label) {
+ return label.getValues().stream()
+ .map(v -> new LabelRemovalPermission.WithValue(label, v))
+ .collect(toSet());
+ }
}
}
diff --git a/java/com/google/gerrit/server/project/DeleteVoteControl.java b/java/com/google/gerrit/server/project/DeleteVoteControl.java
new file mode 100644
index 0000000..3f3f88a
--- /dev/null
+++ b/java/com/google/gerrit/server/project/DeleteVoteControl.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2022 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.project;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.LabelRemovalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import java.util.Set;
+
+public class DeleteVoteControl {
+ private final PermissionBackend permissionBackend;
+ private final ChangeData.Factory changeDataFactory;
+
+ @Inject
+ public DeleteVoteControl(
+ PermissionBackend permissionBackend, ChangeData.Factory changeDataFactory) {
+ this.permissionBackend = permissionBackend;
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ public boolean testDeleteVotePermissions(
+ CurrentUser user, ChangeNotes notes, PatchSetApproval approval, LabelType labelType)
+ throws PermissionBackendException {
+ return testDeleteVotePermissions(user, changeDataFactory.create(notes), approval, labelType);
+ }
+
+ public boolean testDeleteVotePermissions(
+ CurrentUser user, ChangeData cd, PatchSetApproval approval, LabelType labelType)
+ throws PermissionBackendException {
+ if (canRemoveReviewerWithoutRemoveLabelPermission(
+ cd.change(), user, approval.accountId(), approval.value())) {
+ return true;
+ }
+ // Test if the user is allowed to remove vote of the given label type and value.
+ Set<LabelRemovalPermission.WithValue> allowed =
+ permissionBackend.user(user).change(cd).testRemoval(labelType);
+ return allowed.contains(new LabelRemovalPermission.WithValue(labelType, approval.value()));
+ }
+
+ private boolean canRemoveReviewerWithoutRemoveLabelPermission(
+ Change change, CurrentUser user, Account.Id reviewer, int value)
+ throws PermissionBackendException {
+ if (user.isIdentifiedUser()) {
+ Account.Id aId = user.getAccountId();
+ if (aId.equals(reviewer)) {
+ return true; // A user can always remove their own votes.
+ } else if (aId.equals(change.getOwner()) && 0 <= value) {
+ return true; // The change owner may remove any zero or positive score.
+ }
+ }
+
+ // Users with the remove reviewer permission, the branch owner, project
+ // owner and site admin can remove anyone
+ PermissionBackend.WithUser withUser = permissionBackend.user(user);
+ PermissionBackend.ForProject forProject = withUser.project(change.getProject());
+ return forProject.ref(change.getDest().branch()).test(RefPermission.WRITE_CONFIG)
+ || withUser.test(GlobalPermission.ADMINISTRATE_SERVER);
+ }
+}
diff --git a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
index 85a3ab9..df2e1cf 100644
--- a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
@@ -92,7 +92,6 @@
}
@Override
- @SuppressWarnings("CheckReturnValue")
public void run() {
logger.atFine().log("Loading project_list cache");
cache.refreshProjectList();
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 7fdd113..6498d1b 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -300,17 +300,21 @@
@Override
public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
- return Streams.concat(
- Arrays.stream(config.getStringList("groups", /* subsection= */ null, "relevantGroup"))
- .map(AccountGroup::uuid),
- all().stream()
- .map(n -> inMemoryProjectCache.getIfPresent(n))
- .filter(Objects::nonNull)
- .flatMap(p -> p.getAllGroupUUIDs().stream())
- // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
- // against them just in case there is a bug or corner case.
- .filter(id -> id != null && id.get() != null))
- .collect(toSet());
+ Set<AccountGroup.UUID> relevantGroupUuids =
+ Streams.concat(
+ Arrays.stream(
+ config.getStringList("groups", /* subsection= */ null, "relevantGroup"))
+ .map(AccountGroup::uuid),
+ all().stream()
+ .map(n -> inMemoryProjectCache.getIfPresent(n))
+ .filter(Objects::nonNull)
+ .flatMap(p -> p.getAllGroupUUIDs().stream())
+ // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
+ // against them just in case there is a bug or corner case.
+ .filter(id -> id != null && id.get() != null))
+ .collect(toSet());
+ logger.atFine().log("relevant group UUIDs: %s", relevantGroupUuids);
+ return relevantGroupUuids;
}
}
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index 1bc309c..3fda87a 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -32,10 +32,12 @@
@Singleton
public class RemoveReviewerControl {
private final PermissionBackend permissionBackend;
+ private final ChangeData.Factory changeDataFactory;
@Inject
- RemoveReviewerControl(PermissionBackend permissionBackend) {
+ RemoveReviewerControl(PermissionBackend permissionBackend, ChangeData.Factory changeDataFactory) {
this.permissionBackend = permissionBackend;
+ this.changeDataFactory = changeDataFactory;
}
/**
@@ -64,6 +66,20 @@
/** Returns true if the user is allowed to remove this reviewer. */
public boolean testRemoveReviewer(
+ ChangeNotes notes, CurrentUser currentUser, PatchSetApproval approval)
+ throws PermissionBackendException {
+ return testRemoveReviewer(notes, currentUser, approval.accountId(), approval.value());
+ }
+
+ /** Returns true if the user is allowed to remove this reviewer. */
+ public boolean testRemoveReviewer(
+ ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
+ throws PermissionBackendException {
+ return testRemoveReviewer(changeDataFactory.create(notes), currentUser, reviewer, value);
+ }
+
+ /** Returns true if the user is allowed to remove this reviewer. */
+ public boolean testRemoveReviewer(
ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
throws PermissionBackendException {
if (canRemoveReviewerWithoutPermissionCheck(
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index e31411c..d5c4a97 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -159,7 +159,7 @@
return AccountPredicates.preferredEmail(email);
}
- throw new QueryParseException("'email' operator is not supported by account index version");
+ throw new QueryParseException("'email' operator is not supported on this gerrit host");
}
@Operator
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index ec1fcad..fc4df49 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -334,7 +334,7 @@
* Map from {@link com.google.gerrit.entities.Account.Id} to the tip of the edit ref for this
* change and a given user.
*/
- private Table<Account.Id, PatchSet.Id, ObjectId> editsByUser;
+ private Table<Account.Id, PatchSet.Id, Ref> editRefsByUser;
private Set<Account.Id> reviewedBy;
/**
@@ -966,7 +966,7 @@
* submit requirements are evaluated online.
*
* <p>For changes loaded from the index, the value will be set from index field {@link
- * com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS}.
+ * com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS_FIELD}.
*/
public Map<SubmitRequirement, SubmitRequirementResult> submitRequirements() {
if (submitRequirements == null) {
@@ -1097,8 +1097,8 @@
return editRefs().rowKeySet();
}
- public Table<Account.Id, PatchSet.Id, ObjectId> editRefs() {
- if (editsByUser == null) {
+ public Table<Account.Id, PatchSet.Id, Ref> editRefs() {
+ if (editRefsByUser == null) {
if (!lazyload()) {
return HashBasedTable.create();
}
@@ -1106,7 +1106,7 @@
if (c == null) {
return HashBasedTable.create();
}
- editsByUser = HashBasedTable.create();
+ editRefsByUser = HashBasedTable.create();
Change.Id id = requireNonNull(change.getId());
try (Repository repo = repoManager.openRepository(project())) {
for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_USERS)) {
@@ -1117,7 +1117,7 @@
if (id.equals(ps.changeId())) {
Account.Id accountId = Account.Id.fromRef(ref.getName());
if (accountId != null) {
- editsByUser.put(accountId, ps, ref.getObjectId());
+ editRefsByUser.put(accountId, ps, ref);
}
}
}
@@ -1125,7 +1125,7 @@
throw new StorageException(e);
}
}
- return editsByUser;
+ return editRefsByUser;
}
public Set<Account.Id> draftsByUser() {
@@ -1273,13 +1273,13 @@
}
ImmutableSetMultimap.Builder<NameKey, RefState> result = ImmutableSetMultimap.builder();
- for (Table.Cell<Account.Id, PatchSet.Id, ObjectId> edit : editRefs().cellSet()) {
+ for (Table.Cell<Account.Id, PatchSet.Id, Ref> edit : editRefs().cellSet()) {
result.put(
project,
RefState.create(
RefNames.refsEdit(
edit.getRowKey(), edit.getColumnKey().changeId(), edit.getColumnKey()),
- edit.getValue()));
+ edit.getValue().getObjectId()));
}
// TODO: instantiating the notes is too much. We don't want to parse NoteDb, we just want the
@@ -1309,21 +1309,6 @@
.forEach(r -> draftsByUser.put(Account.Id.fromRef(r.ref()), r.id()));
}
}
- if (editsByUser == null) {
- // Recover edit refs as well. Edits are represented as refs in the repository.
- // ChangeData exposes #editsByUser which just provides a Set of Account.Ids of users who
- // have edits on this change. Recovering this list from RefStates makes it available even
- // on ChangeData instances retrieved from the index.
- editsByUser = HashBasedTable.create();
- if (refStates.containsKey(project())) {
- refStates.get(project()).stream()
- .filter(r -> RefNames.isRefsEdit(r.ref()))
- .forEach(
- r ->
- editsByUser.put(
- Account.Id.fromRef(r.ref()), PatchSet.Id.fromEditRef(r.ref()), r.id()));
- }
- }
}
public ImmutableList<byte[]> getRefStatePatterns() {
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 84ceb3d..c344edd 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -336,6 +336,10 @@
return new ChangeIndexPredicate(ChangeField.SUBJECT_SPEC, subject);
}
+ public static Predicate<ChangeData> prefixSubject(String subject) {
+ return new ChangeIndexPredicate(ChangeField.PREFIX_SUBJECT_SPEC, subject);
+ }
+
/**
* Returns a predicate that matches changes where the provided {@code comment} appears in any
* comment on any patch set of the change. Uses full-text search semantics.
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 83c348c..ca18ab2 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -187,6 +187,7 @@
public static final String FIELD_MERGED_ON = "mergedon";
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_SUBJECT = "subject";
+ public static final String FIELD_PREFIX_SUBJECT = "prefixsubject";
public static final String FIELD_MESSAGE_EXACT = "messageexact";
public static final String FIELD_OWNER = "owner";
public static final String FIELD_OWNERIN = "ownerin";
@@ -410,7 +411,7 @@
this.submitRules = submitRules;
}
- Arguments asUser(CurrentUser otherUser) {
+ public Arguments asUser(CurrentUser otherUser) {
return new Arguments(
queryProvider,
rewriter,
@@ -487,13 +488,14 @@
private final Arguments args;
protected Map<String, String> hasOperandAliases = Collections.emptyMap();
private Map<Account.Id, DestinationList> destinationListByAccount = new HashMap<>();
+ private boolean forceAccountVisibilityCheck = false;
private static final Splitter RULE_SPLITTER = Splitter.on("=");
private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
private static final Splitter LABEL_SPLITTER = Splitter.on(",");
@Inject
- ChangeQueryBuilder(Arguments args) {
+ protected ChangeQueryBuilder(Arguments args) {
this(mydef, args);
setupAliases();
}
@@ -513,6 +515,15 @@
return new ChangeQueryBuilder(builderDef, args.asUser(user));
}
+ public Arguments getArgs() {
+ return args;
+ }
+
+ /** Whether to force account visibility check when searching for changes by account(s). */
+ public void forceAccountVisibilityCheck() {
+ forceAccountVisibilityCheck = true;
+ }
+
@Operator
public Predicate<ChangeData> age(String value) {
return new AgePredicate(value);
@@ -540,14 +551,14 @@
@Operator
public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_BEFORE);
+ checkOperatorAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_BEFORE);
return new BeforePredicate(
ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
}
@Operator
public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_AFTER);
+ checkOperatorAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_AFTER);
return new AfterPredicate(
ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
}
@@ -630,7 +641,7 @@
}
if ("attention".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
+ checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
return new IsAttentionPredicate();
}
@@ -673,7 +684,7 @@
}
if ("uploader".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.UPLOADER_SPEC, "is:uploader");
+ checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "is:uploader");
return ChangePredicates.uploader(self());
}
@@ -689,13 +700,14 @@
if ("mergeable".equalsIgnoreCase(value)) {
if (!args.indexMergeable) {
- throw new QueryParseException("'is:mergeable' operator is not supported by server");
+ throw new QueryParseException(
+ "'is:mergeable' operator is not supported on this gerrit host");
}
return new BooleanPredicate(ChangeField.MERGEABLE_SPEC);
}
if ("merge".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.MERGE_SPEC, "is:merge");
+ checkOperatorAvailable(ChangeField.MERGE_SPEC, "is:merge");
return new BooleanPredicate(ChangeField.MERGE_SPEC);
}
@@ -704,7 +716,7 @@
}
if ("attention".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
+ checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
return new IsAttentionPredicate();
}
@@ -717,7 +729,7 @@
}
if ("pure-revert".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert");
+ checkOperatorAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert");
return ChangePredicates.pureRevert("1");
}
@@ -733,12 +745,12 @@
Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
}
- checkFieldAvailable(ChangeField.IS_SUBMITTABLE_SPEC, "is:submittable");
+ checkOperatorAvailable(ChangeField.IS_SUBMITTABLE_SPEC, "is:submittable");
return new IsSubmittablePredicate();
}
if ("started".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.STARTED_SPEC, "is:started");
+ checkOperatorAvailable(ChangeField.STARTED_SPEC, "is:started");
return new BooleanPredicate(ChangeField.STARTED_SPEC);
}
@@ -747,7 +759,7 @@
}
if ("cherrypick".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.CHERRY_PICK_SPEC, "is:cherrypick");
+ checkOperatorAvailable(ChangeField.CHERRY_PICK_SPEC, "is:cherrypick");
return new BooleanPredicate(ChangeField.CHERRY_PICK_SPEC);
}
@@ -770,7 +782,7 @@
@Operator
public Predicate<ChangeData> conflicts(String value) throws QueryParseException {
if (!args.conflictsPredicateEnabled) {
- throw new QueryParseException("'conflicts:' operator is not supported by server");
+ throw new QueryParseException("'conflicts:' operator is not supported on this gerrit host");
}
List<Change> changes = parseChange(value);
List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
@@ -882,7 +894,7 @@
return ChangePredicates.hashtag(hashtag);
}
- checkFieldAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
+ checkOperatorAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
return ChangePredicates.fuzzyHashtag(hashtag);
}
@@ -892,7 +904,7 @@
return ChangePredicates.hashtag(hashtag);
}
- checkFieldAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag");
+ checkOperatorAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag");
return ChangePredicates.prefixHashtag(hashtag);
}
@@ -918,7 +930,7 @@
return ChangePredicates.exactTopic(name);
}
- checkFieldAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic");
+ checkOperatorAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic");
return ChangePredicates.prefixTopic(name);
}
@@ -984,7 +996,7 @@
@Operator
public Predicate<ChangeData> hasfooter(String footerName) throws QueryParseException {
- checkFieldAvailable(ChangeField.FOOTER_NAME, "hasfooter");
+ checkOperatorAvailable(ChangeField.FOOTER_NAME, "hasfooter");
return ChangePredicates.hasFooter(footerName);
}
@@ -1135,7 +1147,9 @@
@Operator
public Predicate<ChangeData> message(String text) throws QueryParseException {
if (text.startsWith("^")) {
- checkFieldAvailable(ChangeField.COMMIT_MESSAGE_EXACT, "messageexact");
+ checkFieldAvailable(
+ ChangeField.COMMIT_MESSAGE_EXACT,
+ "'message' operator with regular expression is not supported on this gerrit host");
return new RegexMessagePredicate(text);
}
return ChangePredicates.message(text);
@@ -1143,10 +1157,17 @@
@Operator
public Predicate<ChangeData> subject(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.SUBJECT_SPEC, ChangeQueryBuilder.FIELD_SUBJECT);
+ checkOperatorAvailable(ChangeField.SUBJECT_SPEC, ChangeQueryBuilder.FIELD_SUBJECT);
return ChangePredicates.subject(value);
}
+ @Operator
+ public Predicate<ChangeData> prefixsubject(String value) throws QueryParseException {
+ checkOperatorAvailable(
+ ChangeField.PREFIX_SUBJECT_SPEC, ChangeQueryBuilder.FIELD_PREFIX_SUBJECT);
+ return ChangePredicates.prefixSubject(value);
+ }
+
private Predicate<ChangeData> starredBySelf() throws QueryParseException {
return ChangePredicates.starBy(
args.starredChangesUtil, self(), StarredChangesUtil.DEFAULT_LABEL);
@@ -1231,7 +1252,7 @@
@Operator
public Predicate<ChangeData> uploader(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploader");
+ checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "uploader");
return uploader(parseAccount(who, (AccountState s) -> true));
}
@@ -1246,7 +1267,7 @@
@Operator
public Predicate<ChangeData> attention(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
+ checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
return attention(parseAccount(who, (AccountState s) -> true));
}
@@ -1291,7 +1312,7 @@
@Operator
public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
- checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploaderin");
+ checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "uploaderin");
GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
@@ -1557,8 +1578,8 @@
@Operator
public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
- checkFieldAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
+ checkOperatorAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
+ checkOperatorAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
if (Ints.tryParse(value) != null) {
return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
}
@@ -1630,11 +1651,16 @@
return Predicate.or(predicates);
}
- protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String operator)
+ private void checkOperatorAvailable(SchemaField<ChangeData, ?> field, String operator)
+ throws QueryParseException {
+ checkFieldAvailable(
+ field, String.format("'%s' operator is not supported on this gerrit host", operator));
+ }
+
+ protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String errorMessage)
throws QueryParseException {
if (!args.index.getSchema().hasField(field)) {
- throw new QueryParseException(
- String.format("'%s' operator is not supported by change index version", operator));
+ throw new QueryParseException(errorMessage);
}
}
@@ -1685,7 +1711,9 @@
private Set<Account.Id> parseAccount(String who)
throws QueryParseException, IOException, ConfigInvalidException {
try {
- return args.accountResolver.resolve(who).asNonEmptyIdSet();
+ return args.accountResolver
+ .resolveAsUser(args.getUser(), who, forceAccountVisibilityCheck)
+ .asNonEmptyIdSet();
} catch (UnresolvableAccountException e) {
if (e.isSelf()) {
throw new QueryRequiresAuthException(e.getMessage(), e);
@@ -1698,7 +1726,9 @@
String who, java.util.function.Predicate<AccountState> activityFilter)
throws QueryParseException, IOException, ConfigInvalidException {
try {
- return args.accountResolver.resolve(who, activityFilter).asNonEmptyIdSet();
+ return args.accountResolver
+ .resolveAsUser(args.getUser(), who, activityFilter, forceAccountVisibilityCheck)
+ .asNonEmptyIdSet();
} catch (UnresolvableAccountException e) {
if (e.isSelf()) {
throw new QueryRequiresAuthException(e.getMessage(), e);
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index ccd645b..3edad69 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -26,6 +26,7 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
@@ -103,6 +104,7 @@
return query(ChangePredicates.idStr(id));
}
+ @UsedAt(UsedAt.Project.GOOGLE)
public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) {
List<Predicate<ChangeData>> preds = new ArrayList<>(ids.size());
for (Change.Id id : ids) {
@@ -115,15 +117,6 @@
return query(byBranchKeyPred(branch, key));
}
- public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key) {
- return query(and(byBranchKeyPred(BranchNameKey.create(project, branch), key), open()));
- }
-
- public static Predicate<ChangeData> byBranchKeyOpenPred(
- Project.NameKey project, String branch, Change.Key key) {
- return and(byBranchKeyPred(BranchNameKey.create(project, branch), key), open());
- }
-
private static Predicate<ChangeData> byBranchKeyPred(BranchNameKey branch, Change.Key key) {
return and(ref(branch), project(branch.project()), change(key));
}
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index 8b4048f..a7b0743 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -25,23 +25,23 @@
/** Utility class to create predicates for project index queries. */
public class ProjectPredicates {
public static Predicate<ProjectData> name(Project.NameKey nameKey) {
- return new ProjectPredicate(ProjectField.NAME, nameKey.get());
+ return new ProjectPredicate(ProjectField.NAME_SPEC, nameKey.get());
}
public static Predicate<ProjectData> parent(Project.NameKey parentNameKey) {
- return new ProjectPredicate(ProjectField.PARENT_NAME, parentNameKey.get());
+ return new ProjectPredicate(ProjectField.PARENT_NAME_SPEC, parentNameKey.get());
}
public static Predicate<ProjectData> inname(String name) {
- return new ProjectPredicate(ProjectField.NAME_PART, name.toLowerCase(Locale.US));
+ return new ProjectPredicate(ProjectField.NAME_PART_SPEC, name.toLowerCase(Locale.US));
}
public static Predicate<ProjectData> description(String description) {
- return new ProjectPredicate(ProjectField.DESCRIPTION, description);
+ return new ProjectPredicate(ProjectField.DESCRIPTION_SPEC, description);
}
public static Predicate<ProjectData> state(ProjectState state) {
- return new ProjectPredicate(ProjectField.STATE, state.name());
+ return new ProjectPredicate(ProjectField.STATE_SPEC, state.name());
}
private ProjectPredicates() {}
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index d234546..edb12ec 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2023 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.
@@ -14,93 +14,21 @@
package com.google.gerrit.server.query.project;
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.index.project.ProjectData;
-import com.google.gerrit.index.query.LimitPredicate;
import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.index.query.QueryParseException;
-import com.google.inject.Inject;
import java.util.List;
-/** Parses a query string meant to be applied to project objects. */
-public class ProjectQueryBuilder extends QueryBuilder<ProjectData, ProjectQueryBuilder> {
- public static final String FIELD_LIMIT = "limit";
+/**
+ * Provides methods required for parsing projects queries.
+ *
+ * <p>Internally (at google), this interface has a different implementation, comparing to upstream.
+ */
+public interface ProjectQueryBuilder {
+ String FIELD_LIMIT = "limit";
- private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilder> mydef =
- new QueryBuilder.Definition<>(ProjectQueryBuilder.class);
-
- @Inject
- ProjectQueryBuilder() {
- super(mydef, null);
- }
-
- @Operator
- public Predicate<ProjectData> name(String name) {
- return ProjectPredicates.name(Project.nameKey(name));
- }
-
- @Operator
- public Predicate<ProjectData> parent(String parentName) {
- return ProjectPredicates.parent(Project.nameKey(parentName));
- }
-
- @Operator
- public Predicate<ProjectData> inname(String namePart) {
- if (namePart.isEmpty()) {
- return name(namePart);
- }
- return ProjectPredicates.inname(namePart);
- }
-
- @Operator
- public Predicate<ProjectData> description(String description) throws QueryParseException {
- if (Strings.isNullOrEmpty(description)) {
- throw error("description operator requires a value");
- }
-
- return ProjectPredicates.description(description);
- }
-
- @Operator
- public Predicate<ProjectData> state(String state) throws QueryParseException {
- if (Strings.isNullOrEmpty(state)) {
- throw error("state operator requires a value");
- }
- ProjectState parsedState;
- try {
- parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
- } catch (IllegalArgumentException e) {
- throw error("state operator must be either 'active' or 'read-only'", e);
- }
- if (parsedState == ProjectState.HIDDEN) {
- throw error("state operator must be either 'active' or 'read-only'");
- }
- return ProjectPredicates.state(parsedState);
- }
-
- @Override
- protected Predicate<ProjectData> defaultField(String query) throws QueryParseException {
- // Adapt the capacity of this list when adding more default predicates.
- List<Predicate<ProjectData>> preds = Lists.newArrayListWithCapacity(3);
- preds.add(name(query));
- preds.add(inname(query));
- if (!Strings.isNullOrEmpty(query)) {
- preds.add(description(query));
- }
- return Predicate.or(preds);
- }
-
- @Operator
- public Predicate<ProjectData> limit(String query) throws QueryParseException {
- Integer limit = Ints.tryParse(query);
- if (limit == null) {
- throw error("Invalid limit: " + query);
- }
- return new LimitPredicate<>(FIELD_LIMIT, limit);
- }
+ /** See {@link com.google.gerrit.index.query.QueryBuilder#parse(String)}. */
+ Predicate<ProjectData> parse(String query) throws QueryParseException;
+ /** See {@link com.google.gerrit.index.query.QueryBuilder#parse(List<String>)}. */
+ List<Predicate<ProjectData>> parse(List<String> queries) throws QueryParseException;
}
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
new file mode 100644
index 0000000..f7135982
--- /dev/null
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
@@ -0,0 +1,105 @@
+// 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.
+
+package com.google.gerrit.server.query.project;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.query.LimitPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryBuilder;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.inject.Inject;
+import java.util.List;
+
+/** Parses a query string meant to be applied to project objects. */
+public class ProjectQueryBuilderImpl extends QueryBuilder<ProjectData, ProjectQueryBuilderImpl>
+ implements ProjectQueryBuilder {
+ private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilderImpl> mydef =
+ new QueryBuilder.Definition<>(ProjectQueryBuilderImpl.class);
+
+ @Inject
+ ProjectQueryBuilderImpl() {
+ super(mydef, null);
+ }
+
+ @Operator
+ public Predicate<ProjectData> name(String name) {
+ return ProjectPredicates.name(Project.nameKey(name));
+ }
+
+ @Operator
+ public Predicate<ProjectData> parent(String parentName) {
+ return ProjectPredicates.parent(Project.nameKey(parentName));
+ }
+
+ @Operator
+ public Predicate<ProjectData> inname(String namePart) {
+ if (namePart.isEmpty()) {
+ return name(namePart);
+ }
+ return ProjectPredicates.inname(namePart);
+ }
+
+ @Operator
+ public Predicate<ProjectData> description(String description) throws QueryParseException {
+ if (Strings.isNullOrEmpty(description)) {
+ throw error("description operator requires a value");
+ }
+
+ return ProjectPredicates.description(description);
+ }
+
+ @Operator
+ public Predicate<ProjectData> state(String state) throws QueryParseException {
+ if (Strings.isNullOrEmpty(state)) {
+ throw error("state operator requires a value");
+ }
+ ProjectState parsedState;
+ try {
+ parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw error("state operator must be either 'active' or 'read-only'", e);
+ }
+ if (parsedState == ProjectState.HIDDEN) {
+ throw error("state operator must be either 'active' or 'read-only'");
+ }
+ return ProjectPredicates.state(parsedState);
+ }
+
+ @Override
+ protected Predicate<ProjectData> defaultField(String query) throws QueryParseException {
+ // Adapt the capacity of this list when adding more default predicates.
+ List<Predicate<ProjectData>> preds = Lists.newArrayListWithCapacity(3);
+ preds.add(name(query));
+ preds.add(inname(query));
+ if (!Strings.isNullOrEmpty(query)) {
+ preds.add(description(query));
+ }
+ return Predicate.or(preds);
+ }
+
+ @Operator
+ public Predicate<ProjectData> limit(String query) throws QueryParseException {
+ Integer limit = Ints.tryParse(query);
+ if (limit == null) {
+ throw error("Invalid limit: " + query);
+ }
+ return new LimitPredicate<>(FIELD_LIMIT, limit);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
index 87d8cd0..f49ee7f 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
@@ -113,6 +113,7 @@
post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class);
post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
+ post(CHANGE_KIND, "rebase:chain").to(RebaseChain.class);
post(CHANGE_KIND, "index").to(Index.class);
post(CHANGE_KIND, "move").to(Move.class);
post(CHANGE_KIND, "private").to(PostPrivate.class);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 7e7892c..c192500 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -19,7 +19,6 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -42,6 +41,7 @@
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.change.ResetCherryPickOp;
import com.google.gerrit.server.change.SetCherryPickOp;
+import com.google.gerrit.server.change.ValidationOptionsUtil;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.CommitUtil;
@@ -72,7 +72,6 @@
import java.time.ZoneId;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
@@ -403,7 +402,8 @@
if (shouldSetToReady(cherryPickCommit, destNotes, workInProgress)) {
inserter.setWorkInProgress(false);
}
- inserter.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
+ inserter.setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
bu.addOp(destChange.getId(), inserter);
PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
// If sourceChange is not provided, reset cherryPickOf to avoid stale value.
@@ -454,7 +454,8 @@
(sourceChange != null && sourceChange.isWorkInProgress())
|| !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
}
- ins.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
+ ins.setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
BranchNameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
ins.setMessage(
@@ -500,20 +501,6 @@
return changeId;
}
- private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
- @Nullable Map<String, String> validationOptions) {
- if (validationOptions == null) {
- return ImmutableListMultimap.of();
- }
-
- ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
- ImmutableListMultimap.builder();
- validationOptions
- .entrySet()
- .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
- return validationOptionsBuilder.build();
- }
-
private NotifyResolver.Result resolveNotify(CherryPickInput input)
throws BadRequestException, ConfigInvalidException, IOException {
return notifyResolver.resolve(
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java b/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
index 432f0da..0e1a218 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
@@ -20,6 +20,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
@@ -37,7 +38,9 @@
import com.google.gerrit.server.mail.send.DeleteVoteSender;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.ReplyToChangeSender;
+import com.google.gerrit.server.permissions.LabelRemovalPermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.DeleteVoteControl;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -75,6 +78,7 @@
private final VoteDeleted voteDeleted;
private final DeleteVoteSender.Factory deleteVoteSenderFactory;
+ private final DeleteVoteControl deleteVoteControl;
private final RemoveReviewerControl removeReviewerControl;
private final MessageIdGenerator messageIdGenerator;
@@ -96,8 +100,9 @@
ChangeMessagesUtil cmUtil,
VoteDeleted voteDeleted,
DeleteVoteSender.Factory deleteVoteSenderFactory,
- RemoveReviewerControl removeReviewerControl,
+ DeleteVoteControl deleteVoteControl,
MessageIdGenerator messageIdGenerator,
+ RemoveReviewerControl removeReviewerControl,
@Assisted Project.NameKey projectName,
@Assisted AccountState reviewerToDeleteVoteFor,
@Assisted String label,
@@ -109,6 +114,7 @@
this.cmUtil = cmUtil;
this.voteDeleted = voteDeleted;
this.deleteVoteSenderFactory = deleteVoteSenderFactory;
+ this.deleteVoteControl = deleteVoteControl;
this.removeReviewerControl = removeReviewerControl;
this.messageIdGenerator = messageIdGenerator;
@@ -143,12 +149,7 @@
newApprovals.put(a.label(), a.value());
continue;
} else if (enforcePermissions) {
- // For regular users, check if they are allowed to remove the vote.
- try {
- removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
- } catch (AuthException e) {
- throw new AuthException("delete vote not permitted", e);
- }
+ checkPermissions(ctx, labelTypes.byLabel(a.labelId()).get(), a);
}
// Set the approval to 0 if vote is being removed.
newApprovals.put(a.label(), (short) 0);
@@ -185,18 +186,16 @@
CurrentUser user = ctx.getUser();
try {
NotifyResolver.Result notify = ctx.getNotify(change.getId());
- if (notify.shouldNotify()) {
- ReplyToChangeSender emailSender =
- deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
- if (user.isIdentifiedUser()) {
- emailSender.setFrom(user.getAccountId());
- }
- emailSender.setChangeMessage(mailMessage, ctx.getWhen());
- emailSender.setNotify(notify);
- emailSender.setMessageId(
- messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
- emailSender.send();
+ ReplyToChangeSender emailSender =
+ deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
+ if (user.isIdentifiedUser()) {
+ emailSender.setFrom(user.getAccountId());
}
+ emailSender.setChangeMessage(mailMessage, ctx.getWhen());
+ emailSender.setNotify(notify);
+ emailSender.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
+ emailSender.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
}
@@ -211,4 +210,21 @@
user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null,
ctx.getWhen());
}
+
+ private void checkPermissions(ChangeContext ctx, LabelType labelType, PatchSetApproval approval)
+ throws PermissionBackendException, AuthException {
+ boolean permitted =
+ removeReviewerControl.testRemoveReviewer(ctx.getNotes(), ctx.getUser(), approval)
+ || deleteVoteControl.testDeleteVotePermissions(
+ ctx.getUser(), ctx.getNotes(), approval, labelType);
+ if (!permitted) {
+ throw new AuthException(
+ "Delete vote not permitted.",
+ new AuthException(
+ "Both "
+ + new LabelRemovalPermission.WithValue(labelType, approval.value())
+ .describeForException()
+ + " and remove-reviewer are not permitted"));
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 9a6b03e..22eb32c 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -17,7 +17,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
-import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.groupingBy;
@@ -379,7 +379,8 @@
// Add the review ops.
logger.atFine().log("posting review");
PostReviewOp postReviewOp =
- postReviewOpFactory.create(projectState, revision.getPatchSet().id(), input);
+ postReviewOpFactory.create(
+ projectState, revision.getPatchSet().id(), input, revision.getAccountId());
bu.addOp(revision.getChange().getId(), postReviewOp);
// Adjust the attention set based on the input
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
index 8a92046..29e453b 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
@@ -65,7 +65,6 @@
import com.google.gerrit.server.approval.ApprovalCopier;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.EmailReviewComments;
-import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.logging.Metadata;
@@ -98,7 +97,8 @@
public class PostReviewOp implements BatchUpdateOp {
interface Factory {
- PostReviewOp create(ProjectState projectState, PatchSet.Id psId, ReviewInput in);
+ PostReviewOp create(
+ ProjectState projectState, PatchSet.Id psId, ReviewInput in, Account.Id reviewerId);
}
/**
@@ -193,6 +193,7 @@
private final ProjectState projectState;
private final PatchSet.Id psId;
private final ReviewInput in;
+ private final Account.Id reviewerId;
private final boolean publishPatchSetLevelComment;
private IdentifiedUser user;
@@ -221,7 +222,8 @@
PluginSetContext<OnPostReview> onPostReviews,
@Assisted ProjectState projectState,
@Assisted PatchSet.Id psId,
- @Assisted ReviewInput in) {
+ @Assisted ReviewInput in,
+ @Assisted Account.Id reviewerId) {
this.approvalCopier = approvalCopier;
this.approvalsUtil = approvalsUtil;
this.publishCommentUtil = publishCommentUtil;
@@ -238,6 +240,7 @@
this.projectState = projectState;
this.psId = psId;
this.in = in;
+ this.reviewerId = reviewerId;
}
@Override
@@ -273,12 +276,9 @@
if (mailMessage == null) {
return;
}
- NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
- if (notify.shouldNotify()) {
- email
- .create(ctx, ps, notes.getMetaId(), mailMessage, comments, in.message, labelDelta)
- .sendAsync();
- }
+ email
+ .create(ctx, ps, notes.getMetaId(), mailMessage, comments, in.message, labelDelta)
+ .sendAsync();
String comment = mailMessage;
if (publishPatchSetLevelComment) {
// TODO(davido): Remove this workaround when patch set level comments are exposed in comment
@@ -649,10 +649,11 @@
del.add(c);
update.putApproval(normName, (short) 0);
}
- // Only allow voting again if the vote is copied over from a past patch-set, or the
- // values are different.
+ // Only allow voting again the values are different, if the real account differs or if the
+ // vote is copied over from a past patch-set.
} else if (c != null
&& (c.value() != ent.getValue()
+ || !c.realAccountId().equals(reviewerId)
|| (inLabels.containsKey(c.label()) && isApprovalCopiedOver(c, ctx.getNotes())))) {
PatchSetApproval.Builder b =
c.toBuilder()
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 5e30dae..8a8d2ca 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -16,38 +16,30 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.change.RebaseUtil;
-import com.google.gerrit.server.change.RebaseUtil.Base;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
@@ -55,13 +47,9 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Map;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
@@ -72,7 +60,6 @@
private final BatchUpdate.Factory updateFactory;
private final GitRepositoryManager repoManager;
- private final RebaseChangeOp.Factory rebaseFactory;
private final RebaseUtil rebaseUtil;
private final ChangeJson.Factory json;
private final PermissionBackend permissionBackend;
@@ -83,7 +70,6 @@
public Rebase(
BatchUpdate.Factory updateFactory,
GitRepositoryManager repoManager,
- RebaseChangeOp.Factory rebaseFactory,
RebaseUtil rebaseUtil,
ChangeJson.Factory json,
PermissionBackend permissionBackend,
@@ -91,7 +77,6 @@
PatchSetUtil patchSetUtil) {
this.updateFactory = updateFactory;
this.repoManager = repoManager;
- this.rebaseFactory = rebaseFactory;
this.rebaseUtil = rebaseUtil;
this.json = json;
this.permissionBackend = permissionBackend;
@@ -102,10 +87,8 @@
@Override
public Response<ChangeInfo> apply(RevisionResource rsrc, RebaseInput input)
throws UpdateException, RestApiException, IOException, PermissionBackendException {
- // Not allowed to rebase if the current patch set is locked.
- patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
-
rsrc.permissions().check(ChangePermission.REBASE);
+
projectCache
.get(rsrc.getProject())
.orElseThrow(illegalState(rsrc.getProject()))
@@ -118,19 +101,14 @@
RevWalk rw = CodeReviewCommit.newRevWalk(reader);
BatchUpdate bu =
updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
- if (!change.isNew()) {
- throw new ResourceConflictException("change is " + ChangeUtil.status(change));
- } else if (!hasOneParent(rw, rsrc.getPatchSet())) {
- throw new ResourceConflictException(
- "cannot rebase merge commits or commit with no ancestor");
- }
+ rebaseUtil.verifyRebasePreconditions(rw, rsrc.getNotes(), rsrc.getPatchSet());
+
RebaseChangeOp rebaseOp =
- rebaseFactory
- .create(rsrc.getNotes(), rsrc.getPatchSet(), findBaseRev(repo, rw, rsrc, input))
- .setForceContentMerge(true)
- .setAllowConflicts(input.allowConflicts)
- .setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions))
- .setFireRevisionCreated(true);
+ rebaseUtil.getRebaseOp(
+ rsrc,
+ input,
+ rebaseUtil.parseOrFindBaseRevision(repo, rw, permissionBackend, rsrc, input, true));
+
// TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
bu.setNotify(NotifyResolver.Result.none());
bu.setRepository(repo, rw, oi);
@@ -144,76 +122,6 @@
}
}
- private ObjectId findBaseRev(
- Repository repo, RevWalk rw, RevisionResource rsrc, RebaseInput input)
- throws RestApiException, IOException, NoSuchChangeException, AuthException,
- PermissionBackendException {
- BranchNameKey destRefKey = rsrc.getChange().getDest();
- if (input == null || input.base == null) {
- return rebaseUtil.findBaseRevision(rsrc.getPatchSet(), destRefKey, repo, rw);
- }
-
- Change change = rsrc.getChange();
- String str = input.base.trim();
- if (str.equals("")) {
- // Remove existing dependency to other patch set.
- Ref destRef = repo.exactRef(destRefKey.branch());
- if (destRef == null) {
- throw new ResourceConflictException(
- "can't rebase onto tip of branch " + destRefKey.branch() + "; branch doesn't exist");
- }
- return destRef.getObjectId();
- }
-
- Base base;
- try {
- base = rebaseUtil.parseBase(rsrc, str);
- if (base == null) {
- throw new ResourceConflictException(
- "base revision is missing from the destination branch: " + str);
- }
- } catch (NoSuchChangeException e) {
- throw new UnprocessableEntityException(
- String.format("Base change not found: %s", input.base), e);
- }
-
- PatchSet.Id baseId = base.patchSet().id();
- if (change.getId().equals(baseId.changeId())) {
- throw new ResourceConflictException("cannot rebase change onto itself");
- }
-
- permissionBackend.user(rsrc.getUser()).change(base.notes()).check(ChangePermission.READ);
-
- Change baseChange = base.notes().getChange();
- if (!baseChange.getProject().equals(change.getProject())) {
- throw new ResourceConflictException(
- "base change is in wrong project: " + baseChange.getProject());
- } else if (!baseChange.getDest().equals(change.getDest())) {
- throw new ResourceConflictException(
- "base change is targeting wrong branch: " + baseChange.getDest());
- } else if (baseChange.isAbandoned()) {
- throw new ResourceConflictException("base change is abandoned: " + baseChange.getKey());
- } else if (isMergedInto(rw, rsrc.getPatchSet(), base.patchSet())) {
- throw new ResourceConflictException(
- "base change "
- + baseChange.getKey()
- + " is a descendant of the current change - recursion not allowed");
- }
- return base.patchSet().commitId();
- }
-
- private boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip) throws IOException {
- ObjectId baseId = base.commitId();
- ObjectId tipId = tip.commitId();
- return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
- }
-
- private boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
- // Prevent rebase of exotic changes (merge commit, no ancestor).
- RevCommit c = rw.parseCommit(ps.commitId());
- return c.getParentCount() == 1;
- }
-
@Override
public UiAction.Description getDescription(RevisionResource rsrc) throws IOException {
UiAction.Description description =
@@ -241,7 +149,7 @@
boolean enabled = false;
try (Repository repo = repoManager.openRepository(change.getDest().project());
RevWalk rw = new RevWalk(repo)) {
- if (hasOneParent(rw, rsrc.getPatchSet())) {
+ if (RebaseUtil.hasOneParent(rw, rsrc.getPatchSet())) {
enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
}
}
@@ -252,20 +160,6 @@
return description;
}
- private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
- @Nullable Map<String, String> validationOptions) {
- if (validationOptions == null) {
- return ImmutableListMultimap.of();
- }
-
- ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
- ImmutableListMultimap.builder();
- validationOptions
- .entrySet()
- .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
- return validationOptionsBuilder.build();
- }
-
public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> {
private final PatchSetUtil psUtil;
private final Rebase rebase;
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
new file mode 100644
index 0000000..786bba7
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -0,0 +1,277 @@
+// Copyright (C) 2022 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.restapi.change;
+
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.RebaseChainInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.GetRelatedChangesUtil;
+import com.google.gerrit.server.change.NotifyResolver;
+import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.change.RebaseUtil;
+import com.google.gerrit.server.change.RelatedChangesSorter.PatchSetData;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Rest API for rebasing an ancestry chain of changes. */
+@Singleton
+public class RebaseChain
+ implements RestModifyView<ChangeResource, RebaseInput>, UiAction<ChangeResource> {
+ private static final ImmutableSet<ListChangesOption> OPTIONS =
+ Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
+
+ private final GitRepositoryManager repoManager;
+ private final RebaseUtil rebaseUtil;
+ private final GetRelatedChangesUtil getRelatedChangesUtil;
+ private final ChangeResource.Factory changeResourceFactory;
+ private final ChangeData.Factory changeDataFactory;
+ private final PermissionBackend permissionBackend;
+ private final BatchUpdate.Factory updateFactory;
+ private final ChangeNotes.Factory notesFactory;
+ private final ProjectCache projectCache;
+ private final PatchSetUtil patchSetUtil;
+ private final ChangeJson.Factory json;
+
+ @Inject
+ RebaseChain(
+ GitRepositoryManager repoManager,
+ RebaseUtil rebaseUtil,
+ GetRelatedChangesUtil getRelatedChangesUtil,
+ ChangeResource.Factory changeResourceFactory,
+ ChangeData.Factory changeDataFactory,
+ PermissionBackend permissionBackend,
+ BatchUpdate.Factory updateFactory,
+ ChangeNotes.Factory notesFactory,
+ ProjectCache projectCache,
+ PatchSetUtil patchSetUtil,
+ ChangeJson.Factory json) {
+ this.repoManager = repoManager;
+ this.getRelatedChangesUtil = getRelatedChangesUtil;
+ this.changeDataFactory = changeDataFactory;
+ this.rebaseUtil = rebaseUtil;
+ this.changeResourceFactory = changeResourceFactory;
+ this.permissionBackend = permissionBackend;
+ this.updateFactory = updateFactory;
+ this.notesFactory = notesFactory;
+ this.projectCache = projectCache;
+ this.patchSetUtil = patchSetUtil;
+ this.json = json;
+ }
+
+ @Override
+ public Response<RebaseChainInfo> apply(ChangeResource tipRsrc, RebaseInput input)
+ throws IOException, PermissionBackendException, RestApiException, UpdateException {
+ tipRsrc.permissions().check(ChangePermission.REBASE);
+
+ Project.NameKey project = tipRsrc.getProject();
+ projectCache.get(project).orElseThrow(illegalState(project)).checkStatePermitsWrite();
+
+ CurrentUser user = tipRsrc.getUser();
+
+ List<Change.Id> upToDateAncestors = new ArrayList<>();
+ Map<Change.Id, RebaseChangeOp> rebaseOps = new LinkedHashMap<>();
+ try (Repository repo = repoManager.openRepository(project);
+ ObjectInserter oi = repo.newObjectInserter();
+ ObjectReader reader = oi.newReader();
+ RevWalk rw = CodeReviewCommit.newRevWalk(reader);
+ BatchUpdate bu = updateFactory.create(project, user, TimeUtil.now())) {
+ List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc);
+
+ boolean ancestorsAreUpToDate = true;
+ for (int i = 0; i < chain.size(); i++) {
+ ChangeData changeData = chain.get(i).data();
+ PatchSet ps = patchSetUtil.current(changeData.notes());
+ if (ps == null) {
+ throw new IllegalStateException(
+ "current revision is missing for change " + changeData.getId());
+ }
+
+ RevisionResource revRsrc =
+ new RevisionResource(changeResourceFactory.create(changeData, user), ps);
+ revRsrc.permissions().check(ChangePermission.REBASE);
+ rebaseUtil.verifyRebasePreconditions(rw, changeData.notes(), ps);
+
+ boolean isUpToDate = false;
+ RebaseChangeOp rebaseOp = null;
+ if (i == 0) {
+ ObjectId desiredBase =
+ rebaseUtil.parseOrFindBaseRevision(
+ repo, rw, permissionBackend, revRsrc, input, false);
+ if (currentBase(rw, ps).equals(desiredBase)) {
+ isUpToDate = true;
+ } else {
+ rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, desiredBase);
+ }
+ } else {
+ if (ancestorsAreUpToDate) {
+ ObjectId latestCommittedBase =
+ PatchSetUtil.getCurrentCommittedRevCommit(
+ project, rw, notesFactory, chain.get(i - 1).id());
+ isUpToDate = currentBase(rw, ps).equals(latestCommittedBase);
+ }
+ if (!isUpToDate) {
+ rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, chain.get(i - 1).id());
+ }
+ }
+
+ if (isUpToDate) {
+ upToDateAncestors.add(changeData.getId());
+ continue;
+ }
+ ancestorsAreUpToDate = false;
+ bu.addOp(revRsrc.getChange().getId(), rebaseOp);
+ rebaseOps.put(revRsrc.getChange().getId(), rebaseOp);
+ }
+
+ if (ancestorsAreUpToDate) {
+ throw new ResourceConflictException("The whole chain is already up to date.");
+ }
+
+ bu.setNotify(NotifyResolver.Result.none());
+ bu.setRepository(repo, rw, oi);
+ bu.execute();
+ }
+
+ RebaseChainInfo res = new RebaseChainInfo();
+ res.rebasedChanges = new ArrayList<>();
+ ChangeJson changeJson = json.create(OPTIONS);
+ for (Change.Id c : upToDateAncestors) {
+ res.rebasedChanges.add(changeJson.format(project, c));
+ }
+ for (Map.Entry<Change.Id, RebaseChangeOp> e : rebaseOps.entrySet()) {
+ Change.Id id = e.getKey();
+ RebaseChangeOp op = e.getValue();
+ ChangeInfo changeInfo = changeJson.format(project, id);
+ changeInfo.containsGitConflicts =
+ !op.getRebasedCommit().getFilesWithGitConflicts().isEmpty() ? true : null;
+ res.rebasedChanges.add(changeInfo);
+ }
+ if (res.rebasedChanges.stream()
+ .anyMatch(i -> i.containsGitConflicts != null && i.containsGitConflicts)) {
+ res.containsGitConflicts = true;
+ }
+ return Response.ok(res);
+ }
+
+ @Override
+ public Description getDescription(ChangeResource tipRsrc) throws Exception {
+ UiAction.Description description =
+ new UiAction.Description()
+ .setLabel("Rebase Chain")
+ .setTitle(
+ "Rebase the ancestry chain onto the tip of the target branch. Makes you the "
+ + "uploader of the changes which can affect validity of approvals.")
+ .setVisible(false);
+
+ Change tip = tipRsrc.getChange();
+ if (!tip.isNew()) {
+ return description;
+ }
+ if (!projectCache
+ .get(tipRsrc.getProject())
+ .orElseThrow(illegalState(tipRsrc.getProject()))
+ .statePermitsWrite()) {
+ return description;
+ }
+
+ if (patchSetUtil.isPatchSetLocked(tipRsrc.getNotes())) {
+ return description;
+ }
+
+ boolean visible = true;
+ boolean enabled = true;
+ try (Repository repo = repoManager.openRepository(tipRsrc.getProject());
+ RevWalk rw = new RevWalk(repo)) {
+ List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc);
+ PatchSetData oldestAncestor = chain.get(0);
+ if (rebaseUtil.canRebase(
+ oldestAncestor.patchSet(), oldestAncestor.data().change().getDest(), repo, rw)) {
+ enabled = false;
+ }
+
+ for (PatchSetData ps : chain) {
+ RevisionResource psRsrc =
+ new RevisionResource(
+ changeResourceFactory.create(ps.data(), tipRsrc.getUser()), ps.patchSet());
+
+ if (!psRsrc.permissions().testOrFalse(ChangePermission.REBASE)) {
+ visible = false;
+ break;
+ }
+
+ if (patchSetUtil.isPatchSetLocked(psRsrc.getNotes())) {
+ enabled = false;
+ }
+ if (!RebaseUtil.hasOneParent(rw, psRsrc.getPatchSet())) {
+ enabled = false;
+ }
+ }
+ }
+ return description.setVisible(visible).setEnabled(enabled);
+ }
+
+ private ObjectId currentBase(RevWalk rw, PatchSet ps) throws IOException {
+ return rw.parseCommit(ps.commitId()).getParent(0);
+ }
+
+ private List<PatchSetData> getChainForCurrentPatchSet(ChangeResource rsrc)
+ throws PermissionBackendException, IOException {
+ return Lists.reverse(
+ getRelatedChangesUtil.getAncestors(
+ changeDataFactory.create(rsrc.getNotes()),
+ patchSetUtil.current(rsrc.getNotes()),
+ true));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index c39b1f4..17fc6db 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -17,8 +17,6 @@
import static com.google.gerrit.entities.RefNames.isConfigRef;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.projects.BranchInfo;
@@ -32,6 +30,7 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ValidationOptionsUtil;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -48,7 +47,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Map;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -160,7 +158,7 @@
rsrc.getName(),
identifiedUser.get(),
u,
- getValidateOptionsAsMultimap(input.validationOptions));
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
RefUpdate.Result result = u.update(rw);
switch (result) {
case FAST_FORWARD:
@@ -220,18 +218,4 @@
private boolean isBranchAllowed(String branch) {
return !RefNames.isGerritRef(branch) && !branch.startsWith(RefNames.REFS_TAGS);
}
-
- private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
- @Nullable Map<String, String> validationOptions) {
- if (validationOptions == null) {
- return ImmutableListMultimap.of();
- }
-
- ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
- ImmutableListMultimap.builder();
- validationOptions
- .entrySet()
- .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
- return validationOptionsBuilder.build();
- }
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateChange.java b/java/com/google/gerrit/server/restapi/project/CreateChange.java
index 59efd06..2f1153e 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateChange.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectResource;
@@ -59,7 +60,8 @@
throw new AuthException("Authentication required");
}
- if (!Strings.isNullOrEmpty(input.project) && !rsrc.getName().equals(input.project)) {
+ if (!Strings.isNullOrEmpty(input.project)
+ && !rsrc.getName().equals(ProjectUtil.sanitizeProjectName(input.project))) {
throw new BadRequestException("project must match URL");
}
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
index 491b5cd..f3c741f 100644
--- a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
+++ b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
@@ -37,6 +37,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+import java.util.stream.Collectors;
import javax.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -216,12 +217,19 @@
Arrays.asList(
cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_VALUE)))
.build();
+ ImmutableList<String> refPatterns =
+ ImmutableList.<String>builder()
+ .addAll(
+ Arrays.asList(
+ cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_BRANCH)))
+ .build();
LabelAttributes attributes =
LabelAttributes.create(
function == null ? "MaxWithBlock" : function,
canOverride,
ignoreSelfApproval,
- values);
+ values,
+ refPatterns);
labelTypes.put(labelName, attributes);
}
return labelTypes;
@@ -320,6 +328,15 @@
default:
break;
}
+ if (!attributes.refPatterns().isEmpty()) {
+ builder.setApplicabilityExpression(
+ SubmitRequirementExpression.of(
+ String.join(
+ " OR ",
+ attributes.refPatterns().stream()
+ .map(b -> "branch:\\\"" + b + "\\\"")
+ .collect(Collectors.toList()))));
+ }
return builder.build();
}
@@ -435,13 +452,16 @@
abstract ImmutableList<String> values();
+ abstract ImmutableList<String> refPatterns();
+
static LabelAttributes create(
String function,
boolean canOverride,
boolean ignoreSelfApproval,
- ImmutableList<String> values) {
+ ImmutableList<String> values,
+ ImmutableList<String> refPatterns) {
return new AutoValue_MigrateLabelFunctionsToSubmitRequirement_LabelAttributes(
- function, canOverride, ignoreSelfApproval, values);
+ function, canOverride, ignoreSelfApproval, values, refPatterns);
}
}
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index a34aeac..14a636f 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -535,9 +535,6 @@
// Multiply the timeout by the number of projects we're actually attempting to
// submit. Times 2 to retry more persistently, to increase success rate.
.defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size() * 2)
- // By default, we only retry lock failures. Here it's better to also retry unexpected
- // runtime exceptions.
- .retryOn(t -> t instanceof RuntimeException)
.call();
submissionExecutor.afterExecutions(orm);
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index f638078..bdda3fc5 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.SubmissionId;
@@ -76,6 +77,8 @@
* merged.
*/
public abstract class SubmitStrategy {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public static Module module() {
return new FactoryModule() {
@Override
@@ -275,6 +278,7 @@
Change.Id id = c.change().getId();
bu.addOp(id, args.setPrivateOpFactory.create(false, null));
ImplicitIntegrateOp implicitIntegrateOp = new ImplicitIntegrateOp(args, c);
+ logger.atFine().log("Add implicit integrate op: %s", implicitIntegrateOp);
bu.addOp(id, implicitIntegrateOp);
maybeAddTestHelperOp(bu, id);
this.submitStrategyOps.add(implicitIntegrateOp);
@@ -282,6 +286,7 @@
// Then ops for explicitly merged changes
for (SubmitStrategyOp op : ops) {
+ logger.atFine().log("Add explicit integrate op: %s", op);
bu.addOp(op.getId(), args.setPrivateOpFactory.create(false, null));
bu.addOp(op.getId(), op);
maybeAddTestHelperOp(bu, op.getId());
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index bb2b1a4..96dc326 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -21,6 +21,7 @@
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
+import com.google.common.base.MoreObjects;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
@@ -127,7 +128,8 @@
logger.atFine().log("No merge tip, no update to perform");
return;
}
- logger.atFine().log("Moved tip from %s to %s", tipBefore, tipAfter);
+ logger.atFine().log(
+ "Moved tip from %s to %s (branch = %s)", tipBefore, tipAfter, getDest().branch());
checkProjectConfig(ctx, tipAfter);
@@ -567,4 +569,14 @@
e);
}
}
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("commit", getCommit().name())
+ .add("changeId", getId())
+ .add("dest", getDest().branch())
+ .add("project", getProject())
+ .toString();
+ }
}
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 32529f7..412a8ee 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -374,8 +374,13 @@
/** Per-change result status from {@link #executeChangeOps}. */
private enum ChangeResult {
+ /** Change was not modified by any of the batch update ops. */
SKIPPED,
+
+ /** Change was inserted or updated. */
UPSERTED,
+
+ /** Change was deleted. */
DELETED
}
@@ -669,7 +674,7 @@
indexFutures.add(indexer.indexAsync(project, id));
break;
case DELETED:
- indexFutures.add(indexer.deleteAsync(id));
+ indexFutures.add(indexer.deleteAsync(project, id));
break;
case SKIPPED:
break;
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index aadf6d4..b828037 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -44,6 +44,7 @@
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -191,6 +192,7 @@
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
install(new AuthModule(authConfig));
install(new GerritApiModule());
+ install(new ProjectQueryBuilderModule());
factory(PluginUser.Factory.class);
install(new PluginApiModule());
install(new DefaultPermissionBackendModule());
diff --git a/java/com/google/gerrit/testing/TestChanges.java b/java/com/google/gerrit/testing/TestChanges.java
index 4a97bc5..c8f89cf 100644
--- a/java/com/google/gerrit/testing/TestChanges.java
+++ b/java/com/google/gerrit/testing/TestChanges.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Injector;
import java.time.ZoneId;
+import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
@@ -77,15 +78,20 @@
}
public static ChangeUpdate newUpdate(
- Injector injector, Change c, CurrentUser user, boolean shouldExist) throws Exception {
+ Injector injector, Change c, Optional<CurrentUser> user, boolean shouldExist)
+ throws Exception {
injector =
injector.createChildInjector(
new FactoryModule() {
@Override
public void configure() {
- bind(CurrentUser.class).toInstance(user);
+ if (user.isPresent()) {
+ // user may be already bound in injector
+ bind(CurrentUser.class).toInstance(user.get());
+ }
}
});
+ CurrentUser currentUser = injector.getProvider(CurrentUser.class).get();
ChangeUpdate update =
injector
.getInstance(ChangeUpdate.Factory.class)
@@ -93,7 +99,7 @@
new ChangeNotes(
injector.getInstance(AbstractChangeNotes.Args.class), c, shouldExist, null)
.load(),
- user,
+ currentUser,
TimeUtil.now(),
Ordering.natural());
@@ -109,7 +115,9 @@
try (Repository repo = repoManager.openRepository(c.getProject());
TestRepository<Repository> tr = new TestRepository<>(repo)) {
PersonIdent ident =
- user.asIdentifiedUser().newCommitterIdent(update.getWhen(), ZoneId.systemDefault());
+ currentUser
+ .asIdentifiedUser()
+ .newCommitterIdent(update.getWhen(), ZoneId.systemDefault());
TestRepository<Repository>.CommitBuilder cb =
tr.commit()
.author(ident)
diff --git a/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java b/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java
new file mode 100644
index 0000000..27e4b17
--- /dev/null
+++ b/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2023 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.util.cli;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.reflect.ClassPath;
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Utility to generate Protocol Buffers (*.proto) files from existing POJO API types.
+ *
+ * <p>Usage:
+ *
+ * <ul>
+ * <li>Print proto representation of all API objects: {@code bazelisk run
+ * java/com/google/gerrit/util/cli:protogen}
+ * </ul>
+ */
+public class ApiProtocolBufferGenerator {
+ private static String NOTICE =
+ "// Copyright (C) 2023 The Android Open Source Project\n"
+ + "//\n"
+ + "// Licensed under the Apache License, Version 2.0 (the \"License\");\n"
+ + "// you may not use this file except in compliance with the License.\n"
+ + "// You may obtain a copy of the License at\n"
+ + "//\n"
+ + "// http://www.apache.org/licenses/LICENSE-2.0\n"
+ + "//\n"
+ + "// Unless required by applicable law or agreed to in writing, software\n"
+ + "// distributed under the License is distributed on an \"AS IS\" BASIS,\n"
+ + "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"
+ + "// See the License for the specific language governing permissions and\n"
+ + "// limitations under the License.";
+
+ private static String PACKAGE = "com.google.gerrit.extensions.common";
+
+ public static void main(String[] args) {
+ try {
+ ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses().stream()
+ .filter(c -> c.getPackageName().equalsIgnoreCase(PACKAGE))
+ .filter(c -> c.getName().endsWith("Input") || c.getName().endsWith("Info"))
+ .map(clazz -> clazz.load())
+ .forEach(ApiProtocolBufferGenerator::exportSingleClass);
+ } catch (Exception e) {
+ System.err.println(e);
+ }
+ }
+
+ private static void exportSingleClass(Class<?> clazz) {
+ StringBuilder proto = new StringBuilder(NOTICE);
+ proto.append("\n\nsyntax = \"proto3\";");
+ proto.append("\n\npackage gerrit.api;");
+ proto.append("\n\noption java_package = \"" + PACKAGE + "\";");
+
+ int fieldNumber = 1;
+
+ proto.append("\n\n\nmessage " + clazz.getSimpleName() + " {\n");
+
+ for (Field f : clazz.getFields()) {
+ Class<?> type = f.getType();
+
+ if (type.isAssignableFrom(List.class)) {
+ ParameterizedType list = (ParameterizedType) f.getGenericType();
+ Class<?> genericType = (Class<?>) list.getActualTypeArguments()[0];
+ String protoType =
+ protoType(genericType)
+ .orElseThrow(() -> new IllegalStateException("unknown type: " + genericType));
+ proto.append(
+ String.format(
+ "repeated %s %s = %d;\n", protoType, protoName(f.getName()), fieldNumber));
+ } else if (type.isAssignableFrom(Map.class)) {
+ ParameterizedType map = (ParameterizedType) f.getGenericType();
+ Class<?> key = (Class<?>) map.getActualTypeArguments()[0];
+ if (map.getActualTypeArguments()[1] instanceof ParameterizedType) {
+ // TODO: This is list multimap which proto doesn't support. Move to
+ // it's own types.
+ proto.append(
+ "reserved "
+ + fieldNumber
+ + "; // TODO(hiesel): Add support for map<?,repeated <?>>\n");
+ } else {
+ Class<?> value = (Class<?>) map.getActualTypeArguments()[1];
+ String keyProtoType =
+ protoType(key).orElseThrow(() -> new IllegalStateException("unknown type: " + key));
+ String valueProtoType =
+ protoType(value)
+ .orElseThrow(() -> new IllegalStateException("unknown type: " + value));
+ proto.append(
+ String.format(
+ "map<%s,%s> %s = %d;\n",
+ keyProtoType, valueProtoType, protoName(f.getName()), fieldNumber));
+ }
+ } else if (protoType(type).isPresent()) {
+ proto.append(
+ String.format(
+ "%s %s = %d;\n", protoType(type).get(), protoName(f.getName()), fieldNumber));
+ } else {
+ proto.append(
+ "reserved "
+ + fieldNumber
+ + "; // TODO(hiesel): Add support for "
+ + type.getName()
+ + "\n");
+ }
+ fieldNumber++;
+ }
+ proto.append("}");
+
+ System.out.println(proto);
+ }
+
+ private static Optional<String> protoType(Class<?> type) {
+ if (isInt(type)) {
+ return Optional.of("int32");
+ } else if (isLong(type)) {
+ return Optional.of("int64");
+ } else if (isChar(type)) {
+ return Optional.of("string");
+ } else if (isShort(type)) {
+ return Optional.of("int32");
+ } else if (isShort(type)) {
+ return Optional.of("int32");
+ } else if (isBoolean(type)) {
+ return Optional.of("bool");
+ } else if (type.isAssignableFrom(String.class)) {
+ return Optional.of("string");
+ } else if (type.isAssignableFrom(Timestamp.class)) {
+ // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp
+ return Optional.of("string");
+ } else if (type.getPackageName().startsWith("com.google.gerrit.extensions")) {
+ return Optional.of("gerrit.api." + type.getSimpleName());
+ }
+ return Optional.empty();
+ }
+
+ private static boolean isInt(Class<?> type) {
+ return type.isAssignableFrom(Integer.class) || type.isAssignableFrom(int.class);
+ }
+
+ private static boolean isLong(Class<?> type) {
+ return type.isAssignableFrom(Long.class) || type.isAssignableFrom(long.class);
+ }
+
+ private static boolean isChar(Class<?> type) {
+ return type.isAssignableFrom(Character.class) || type.isAssignableFrom(char.class);
+ }
+
+ private static boolean isShort(Class<?> type) {
+ return type.isAssignableFrom(Short.class) || type.isAssignableFrom(short.class);
+ }
+
+ private static boolean isBoolean(Class<?> type) {
+ return type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class);
+ }
+
+ private static String protoName(String name) {
+ return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name);
+ }
+}
diff --git a/java/com/google/gerrit/util/cli/BUILD b/java/com/google/gerrit/util/cli/BUILD
index ebcc67e..b464f32 100644
--- a/java/com/google/gerrit/util/cli/BUILD
+++ b/java/com/google/gerrit/util/cli/BUILD
@@ -2,7 +2,10 @@
java_library(
name = "cli",
- srcs = glob(["**/*.java"]),
+ srcs = glob(
+ ["**/*.java"],
+ exclude = ["ApiProtocolBufferGenerator.java"],
+ ),
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/common:annotations",
@@ -14,3 +17,15 @@
"//lib/guice:guice-assistedinject",
],
)
+
+# Util to generate *.proto files from *Info and *Input objects
+java_binary(
+ name = "protogen",
+ srcs = ["ApiProtocolBufferGenerator.java"],
+ main_class = "com.google.gerrit.util.cli.ApiProtocolBufferGenerator",
+ deps = [
+ "//java/com/google/gerrit/extensions:api",
+ "//lib:guava",
+ "//lib:protobuf",
+ ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 76009bf..215d1e8 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -28,6 +28,7 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabelRemoval;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
@@ -47,7 +48,6 @@
import static com.google.gerrit.extensions.client.ReviewerState.CC;
import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
-import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
@@ -62,7 +62,6 @@
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
-import static org.eclipse.jgit.lib.Constants.HEAD;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheStats;
@@ -118,9 +117,7 @@
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.NotifyInfo;
-import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
@@ -148,33 +145,27 @@
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.events.AttentionSetListener;
-import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.testing.TestChangeETagComputation;
-import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.ChangeMessageModifier;
-import com.google.gerrit.server.git.validators.CommitValidationException;
-import com.google.gerrit.server.git.validators.CommitValidationListener;
-import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -196,7 +187,6 @@
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.name.Named;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.Timestamp;
import java.text.MessageFormat;
@@ -218,7 +208,6 @@
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
@@ -241,6 +230,7 @@
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
@Inject private IndexOperations.Change changeIndexOperations;
+ @Inject private AccountControl.Factory accountControlFactory;
@Inject
@Named("diff_intraline")
@@ -779,366 +769,6 @@
assertThat(thrown).hasMessageThat().contains("Multiple changes found for " + changeId);
}
- @FunctionalInterface
- private interface Rebase {
- void call(String id) throws RestApiException;
- }
-
- @Test
- public void rebaseViaRevisionApi() throws Exception {
- testRebase(id -> gApi.changes().id(id).current().rebase());
- }
-
- @Test
- public void rebaseViaChangeApi() throws Exception {
- testRebase(id -> gApi.changes().id(id).rebase());
- }
-
- private void testRebase(Rebase rebase) throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- // Add an approval whose score should be copied on trivial rebase
- gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
-
- String changeId = r2.getChangeId();
- // Rebase the second change
- rebase.call(changeId);
-
- // Second change should have 2 patch sets and an approval
- ChangeInfo c2 = gApi.changes().id(changeId).get(CURRENT_REVISION, DETAILED_LABELS);
- assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2);
-
- // ...and the committer and description should be correct
- ChangeInfo info = gApi.changes().id(changeId).get(CURRENT_REVISION, CURRENT_COMMIT);
- GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
- assertThat(committer.name).isEqualTo(admin.fullName());
- assertThat(committer.email).isEqualTo(admin.email());
- String description = info.revisions.get(info.currentRevision).description;
- assertThat(description).isEqualTo("Rebase");
-
- // ...and the approval was copied
- LabelInfo cr = c2.labels.get(LabelId.CODE_REVIEW);
- assertThat(cr).isNotNull();
- assertThat(cr.all).hasSize(1);
- assertThat(cr.all.get(0).value).isEqualTo(1);
-
- // Rebasing the second change again should fail
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class, () -> gApi.changes().id(changeId).current().rebase());
- assertThat(thrown).hasMessageThat().contains("Change is already up to date");
- }
-
- @Test
- public void rebaseAsUploaderInAttentionSet() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- TestAccount admin2 = accountCreator.admin2();
- requestScopeOperations.setApiUser(admin2.id());
- amendChangeWithUploader(r2, project, admin2);
- gApi.changes()
- .id(r2.getChangeId())
- .addToAttentionSet(new AttentionSetInput(admin2.id().toString(), "manual update"));
-
- gApi.changes().id(r2.getChangeId()).rebase();
- }
-
- @Test
- public void rebaseOnChangeNumber() throws Exception {
- String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
- PushOneCommit.Result r1 = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
- assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
-
- Change.Id id1 = r1.getChange().getId();
- RebaseInput in = new RebaseInput();
- in.base = id1.toString();
- gApi.changes().id(r2.getChangeId()).rebase(in);
-
- Change.Id id2 = r2.getChange().getId();
- ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- ri2 = ci2.revisions.get(ci2.currentRevision);
- assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
-
- List<RelatedChangeAndCommitInfo> related =
- gApi.changes().id(id2.get()).revision(ri2._number).related().changes;
- assertThat(related).hasSize(2);
- assertThat(related.get(0)._changeNumber).isEqualTo(id2.get());
- assertThat(related.get(0)._revisionNumber).isEqualTo(2);
- assertThat(related.get(1)._changeNumber).isEqualTo(id1.get());
- assertThat(related.get(1)._revisionNumber).isEqualTo(1);
- }
-
- @Test
- public void rebaseOnClosedChange() throws Exception {
- String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
- PushOneCommit.Result r1 = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
- assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
-
- // Submit first change.
- Change.Id id1 = r1.getChange().getId();
- gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
- gApi.changes().id(id1.get()).current().submit();
-
- // Rebase second change on first change.
- RebaseInput in = new RebaseInput();
- in.base = id1.toString();
- gApi.changes().id(r2.getChangeId()).rebase(in);
-
- Change.Id id2 = r2.getChange().getId();
- ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- ri2 = ci2.revisions.get(ci2.currentRevision);
- assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
-
- assertThat(gApi.changes().id(id2.get()).revision(ri2._number).related().changes).isEmpty();
- }
-
- @Test
- public void rebaseOnNonExistingChange() throws Exception {
- String changeId = createChange().getChangeId();
- RebaseInput in = new RebaseInput();
- in.base = "999999";
- UnprocessableEntityException exception =
- assertThrows(
- UnprocessableEntityException.class, () -> gApi.changes().id(changeId).rebase(in));
- assertThat(exception).hasMessageThat().isEqualTo("Base change not found: " + in.base);
- }
-
- @Test
- public void rebaseFromRelationChainToClosedChange() throws Exception {
- PushOneCommit.Result r1 = createChange();
- testRepo.reset("HEAD~1");
-
- createChange();
- PushOneCommit.Result r3 = createChange();
-
- // Submit first change.
- Change.Id id1 = r1.getChange().getId();
- gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
- gApi.changes().id(id1.get()).current().submit();
-
- // Rebase third change on first change.
- RebaseInput in = new RebaseInput();
- in.base = id1.toString();
- gApi.changes().id(r3.getChangeId()).rebase(in);
-
- Change.Id id3 = r3.getChange().getId();
- ChangeInfo ci3 = get(r3.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- RevisionInfo ri3 = ci3.revisions.get(ci3.currentRevision);
- assertThat(ri3.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
-
- assertThat(gApi.changes().id(id3.get()).revision(ri3._number).related().changes).isEmpty();
- }
-
- @Test
- public void rebaseNotAllowedWithoutPermission() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- // Rebase the second
- String changeId = r2.getChangeId();
- requestScopeOperations.setApiUser(user.id());
- AuthException thrown =
- assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
- assertThat(thrown).hasMessageThat().contains("rebase not permitted");
- }
-
- @Test
- public void rebaseAllowedWithPermission() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- projectOperations
- .project(project)
- .forUpdate()
- .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
- .update();
-
- // Rebase the second
- String changeId = r2.getChangeId();
- requestScopeOperations.setApiUser(user.id());
- gApi.changes().id(changeId).rebase();
- }
-
- @Test
- public void rebaseNotAllowedWithoutPushPermission() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- projectOperations
- .project(project)
- .forUpdate()
- .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
- .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
- .update();
-
- // Rebase the second
- String changeId = r2.getChangeId();
- requestScopeOperations.setApiUser(user.id());
- AuthException thrown =
- assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
- assertThat(thrown).hasMessageThat().contains("rebase not permitted");
- }
-
- @Test
- public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- projectOperations
- .project(project)
- .forUpdate()
- .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
- .update();
-
- // Rebase the second
- String changeId = r2.getChangeId();
- AuthException thrown =
- assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
- assertThat(thrown).hasMessageThat().contains("rebase not permitted");
- }
-
- @Test
- public void rebaseWithValidationOptions() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- RebaseInput rebaseInput = new RebaseInput();
- rebaseInput.validationOptions = ImmutableMap.of("key", "value");
-
- TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener();
- try (Registration registration =
- extensionRegistry.newRegistration().add(testCommitValidationListener)) {
- // Rebase the second change
- gApi.changes().id(r2.getChangeId()).current().rebase(rebaseInput);
- assertThat(testCommitValidationListener.receiveEvent.pushOptions)
- .containsExactly("key", "value");
- }
- }
-
- @Test
- public void rebaseOutdatedPatchSet() throws Exception {
- String fileName1 = "a.txt";
- String fileContent1 = "some content";
- String fileName2 = "b.txt";
- String fileContent2Ps1 = "foo";
- String fileContent2Ps2 = "foo/bar";
-
- // Create two changes both with the same parent touching disjunct files
- PushOneCommit.Result r =
- pushFactory
- .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName1, fileContent1)
- .to("refs/for/master");
- r.assertOkStatus();
- String changeId1 = r.getChangeId();
- testRepo.reset("HEAD~1");
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName2, fileContent2Ps1);
- PushOneCommit.Result r2 = push.to("refs/for/master");
- r2.assertOkStatus();
- String changeId2 = r2.getChangeId();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(changeId1).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- // Amend the second change so that it has 2 patch sets
- amendChange(
- changeId2,
- "refs/for/master",
- admin,
- testRepo,
- PushOneCommit.SUBJECT,
- fileName2,
- fileContent2Ps2)
- .assertOkStatus();
- ChangeInfo changeInfo2 = gApi.changes().id(changeId2).get();
- assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(2);
-
- // Rebase the first patch set of the second change
- gApi.changes().id(changeId2).revision(1).rebase();
-
- // Second change should have 3 patch sets
- changeInfo2 = gApi.changes().id(changeId2).get();
- assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(3);
-
- // ... and the committer and description should be correct
- ChangeInfo info = gApi.changes().id(changeId2).get(CURRENT_REVISION, CURRENT_COMMIT);
- GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
- assertThat(committer.name).isEqualTo(admin.fullName());
- assertThat(committer.email).isEqualTo(admin.email());
- String description = info.revisions.get(info.currentRevision).description;
- assertThat(description).isEqualTo("Rebase");
-
- // ... and the file contents should match with patch set 1 based on change1
- assertThat(gApi.changes().id(changeId2).current().file(fileName1).content().asString())
- .isEqualTo(fileContent1);
- assertThat(gApi.changes().id(changeId2).current().file(fileName2).content().asString())
- .isEqualTo(fileContent2Ps1);
- }
-
@Test
public void deleteNewChangeAsAdmin() throws Exception {
deleteChangeAsUser(admin, admin);
@@ -1471,166 +1101,6 @@
}
@Test
- public void rebaseUpToDateChange() throws Exception {
- PushOneCommit.Result r = createChange();
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase());
- assertThat(thrown).hasMessageThat().contains("Change is already up to date");
- }
-
- @Test
- public void rebaseConflict() throws Exception {
- PushOneCommit.Result r1 = createChange();
- gApi.changes()
- .id(r1.getChangeId())
- .revision(r1.getCommit().name())
- .review(ReviewInput.approve());
- gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
-
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(),
- testRepo,
- PushOneCommit.SUBJECT,
- PushOneCommit.FILE_NAME,
- "other content",
- "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5");
- PushOneCommit.Result r2 = push.to("refs/for/master");
- r2.assertOkStatus();
- ResourceConflictException exception =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase());
- assertThat(exception)
- .hasMessageThat()
- .isEqualTo(
- String.format(
- "The change could not be rebased due to a conflict during merge.\n\n"
- + "merge conflict(s):\n%s",
- PushOneCommit.FILE_NAME));
- }
-
- @Test
- public void rebaseDoesNotAddWorkInProgress() throws Exception {
- PushOneCommit.Result r = createChange();
-
- // create an unrelated change so that we can rebase
- testRepo.reset("HEAD~1");
- PushOneCommit.Result unrelated = createChange();
- gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
- gApi.changes().id(unrelated.getChangeId()).current().submit();
-
- gApi.changes().id(r.getChangeId()).rebase();
-
- // change is still ready for review after rebase
- assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isNull();
- }
-
- @Test
- public void rebaseDoesNotRemoveWorkInProgress() throws Exception {
- PushOneCommit.Result r = createChange();
- change(r).setWorkInProgress();
-
- // create an unrelated change so that we can rebase
- testRepo.reset("HEAD~1");
- PushOneCommit.Result unrelated = createChange();
- gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
- gApi.changes().id(unrelated.getChangeId()).current().submit();
-
- gApi.changes().id(r.getChangeId()).rebase();
-
- // change is still work in progress after rebase
- assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isTrue();
- }
-
- @Test
- public void rebaseConflict_conflictsAllowed() throws Exception {
- String patchSetSubject = "patch set change";
- String patchSetContent = "patch set content";
- String baseSubject = "base change";
- String baseContent = "base content";
-
- PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
- gApi.changes()
- .id(r1.getChangeId())
- .revision(r1.getCommit().name())
- .review(ReviewInput.approve());
- gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
-
- testRepo.reset("HEAD~1");
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(), testRepo, patchSetSubject, PushOneCommit.FILE_NAME, patchSetContent);
- PushOneCommit.Result r2 = push.to("refs/for/master");
- r2.assertOkStatus();
-
- String changeId = r2.getChangeId();
- RevCommit patchSet = r2.getCommit();
- RevCommit base = r1.getCommit();
-
- TestWorkInProgressStateChangedListener wipStateChangedListener =
- new TestWorkInProgressStateChangedListener();
- try (Registration registration =
- extensionRegistry.newRegistration().add(wipStateChangedListener)) {
- RebaseInput rebaseInput = new RebaseInput();
- rebaseInput.allowConflicts = true;
- ChangeInfo changeInfo =
- gApi.changes().id(changeId).revision(patchSet.name()).rebaseAsInfo(rebaseInput);
- assertThat(changeInfo.containsGitConflicts).isTrue();
- assertThat(changeInfo.workInProgress).isTrue();
- }
- assertThat(wipStateChangedListener.invoked).isTrue();
- assertThat(wipStateChangedListener.wip).isTrue();
-
- // To get the revisions, we must retrieve the change with more change options.
- ChangeInfo changeInfo =
- gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
- assertThat(changeInfo.revisions).hasSize(2);
- assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
- .isEqualTo(base.name());
-
- // Verify that the file content in the created patch set is correct.
- // We expect that it has conflict markers to indicate the conflict.
- BinaryResult bin =
- gApi.changes().id(changeId).current().file(PushOneCommit.FILE_NAME).content();
- ByteArrayOutputStream os = new ByteArrayOutputStream();
- bin.writeTo(os);
- String fileContent = new String(os.toByteArray(), UTF_8);
- String patchSetSha1 = abbreviateName(patchSet, 6);
- String baseSha1 = abbreviateName(base, 6);
- assertThat(fileContent)
- .isEqualTo(
- "<<<<<<< PATCH SET ("
- + patchSetSha1
- + " "
- + patchSetSubject
- + ")\n"
- + patchSetContent
- + "\n"
- + "=======\n"
- + baseContent
- + "\n"
- + ">>>>>>> BASE ("
- + baseSha1
- + " "
- + baseSubject
- + ")\n");
-
- // Verify the message that has been posted on the change.
- List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
- assertThat(messages).hasSize(2);
- assertThat(Iterables.getLast(messages).message)
- .isEqualTo(
- "Patch Set 2: Patch Set 1 was rebased\n\n"
- + "The following files contain Git conflicts:\n"
- + "* "
- + PushOneCommit.FILE_NAME
- + "\n");
- }
-
- @Test
public void attentionSetListener_firesOnChange() throws Exception {
PushOneCommit.Result r1 = createChange();
AttentionSetInput addUser = new AttentionSetInput(user.email(), "some reason");
@@ -1663,156 +1133,6 @@
}
@Test
- public void rebaseChangeBase() throws Exception {
- PushOneCommit.Result r1 = createChange();
- PushOneCommit.Result r2 = createChange();
- PushOneCommit.Result r3 = createChange();
- RebaseInput ri = new RebaseInput();
-
- // rebase r3 directly onto master (break dep. towards r2)
- ri.base = "";
- gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).rebase(ri);
- PatchSet ps3 = r3.getPatchSet();
- assertThat(ps3.id().get()).isEqualTo(2);
-
- // rebase r2 onto r3 (referenced by ref)
- ri.base = ps3.id().toRefName();
- gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri);
- PatchSet ps2 = r2.getPatchSet();
- assertThat(ps2.id().get()).isEqualTo(2);
-
- // rebase r1 onto r2 (referenced by commit)
- ri.base = ps2.commitId().name();
- gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri);
- PatchSet ps1 = r1.getPatchSet();
- assertThat(ps1.id().get()).isEqualTo(2);
-
- // rebase r1 onto r3 (referenced by change number)
- ri.base = String.valueOf(r3.getChange().getId().get());
- gApi.changes().id(r1.getChangeId()).revision(ps1.commitId().name()).rebase(ri);
- assertThat(r1.getPatchSetId().get()).isEqualTo(3);
- }
-
- @Test
- public void rebaseChangeBaseRecursion() throws Exception {
- PushOneCommit.Result r1 = createChange();
- PushOneCommit.Result r2 = createChange();
-
- RebaseInput ri = new RebaseInput();
- ri.base = r2.getCommit().name();
- String expectedMessage =
- "base change "
- + r2.getChangeId()
- + " is a descendant of the current change - recursion not allowed";
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri));
- assertThat(thrown).hasMessageThat().contains(expectedMessage);
- }
-
- @Test
- public void rebaseAbandonedChange() throws Exception {
- PushOneCommit.Result r = createChange();
- String changeId = r.getChangeId();
- assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
- gApi.changes().id(changeId).abandon();
- ChangeInfo info = info(changeId);
- assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
-
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(changeId).revision(r.getCommit().name()).rebase());
- assertThat(thrown).hasMessageThat().contains("change is abandoned");
- }
-
- @Test
- public void rebaseOntoAbandonedChange() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Abandon the first change
- String changeId = r.getChangeId();
- assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
- gApi.changes().id(changeId).abandon();
- ChangeInfo info = info(changeId);
- assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
-
- RebaseInput ri = new RebaseInput();
- ri.base = r.getCommit().name();
-
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri));
- assertThat(thrown).hasMessageThat().contains("base change is abandoned: " + changeId);
- }
-
- @Test
- public void rebaseOntoSelf() throws Exception {
- PushOneCommit.Result r = createChange();
- String changeId = r.getChangeId();
- String commit = r.getCommit().name();
- RebaseInput ri = new RebaseInput();
- ri.base = commit;
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(changeId).revision(commit).rebase(ri));
- assertThat(thrown).hasMessageThat().contains("cannot rebase change onto itself");
- }
-
- @Test
- public void cannotRebaseOntoBaseThatIsNotPresentInTargetBranch() throws Exception {
- ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
-
- BranchInput branchInput = new BranchInput();
- branchInput.revision = initial.getName();
- gApi.projects().name(project.get()).branch("foo").create(branchInput);
-
- PushOneCommit.Result r1 =
- pushFactory
- .create(admin.newIdent(), testRepo, "Change on foo branch", "a.txt", "a-content")
- .to("refs/for/foo");
- approve(r1.getChangeId());
- gApi.changes().id(r1.getChangeId()).current().submit();
-
- // reset HEAD in order to create a sibling of the first change
- testRepo.reset(initial);
-
- PushOneCommit.Result r2 =
- pushFactory
- .create(admin.newIdent(), testRepo, "Change on master branch", "b.txt", "b-content")
- .to("refs/for/master");
-
- RebaseInput rebaseInput = new RebaseInput();
- rebaseInput.base = r1.getCommit().getName();
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r2.getChangeId()).current().rebase(rebaseInput));
- assertThat(thrown)
- .hasMessageThat()
- .contains(
- String.format(
- "base change is targeting wrong branch: %s,refs/heads/foo", project.get()));
-
- rebaseInput.base = "refs/heads/foo";
- thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r2.getChangeId()).current().rebase(rebaseInput));
- assertThat(thrown)
- .hasMessageThat()
- .contains(
- String.format(
- "base revision is missing from the destination branch: %s", rebaseInput.base));
- }
-
- @Test
@TestProjectInput(createEmptyCommit = false)
public void changeNoParentToOneParent() throws Exception {
// create initial commit with no parent and push it as change, so that patch
@@ -2805,6 +2125,78 @@
}
@Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void removeNonVisibleReviewer() throws Exception {
+ // allow all users to remove reviewers
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ gApi.changes().id(changeId).addReviewer(user.email());
+ AccountInfo reviewerInfo =
+ Iterables.getOnlyElement(
+ gApi.changes().id(changeId).get().reviewers.get(ReviewerState.REVIEWER));
+ assertThat(reviewerInfo._accountId).isEqualTo(user.id().get());
+
+ TestAccount user2 = accountCreator.user2();
+ requestScopeOperations.setApiUser(user2.id());
+
+ // user2 cannot see user
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isFalse();
+
+ gApi.changes().id(changeId).reviewer(user.id().toString()).remove(new DeleteReviewerInput());
+ assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void removeNonVisibleReviewerThroughPostReview() throws Exception {
+ // allow all users to remove reviewers
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ gApi.changes().id(changeId).addReviewer(user.email());
+ AccountInfo reviewerInfo =
+ Iterables.getOnlyElement(
+ gApi.changes().id(changeId).get().reviewers.get(ReviewerState.REVIEWER));
+ assertThat(reviewerInfo._accountId).isEqualTo(user.id().get());
+
+ TestAccount user2 = accountCreator.user2();
+ requestScopeOperations.setApiUser(user2.id());
+
+ // user2 cannot see user
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isFalse();
+
+ ReviewerInput reviewerInput = new ReviewerInput();
+ reviewerInput.reviewer = user.email();
+ reviewerInput.state = ReviewerState.REMOVED;
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.reviewers = ImmutableList.of(reviewerInput);
+ ReviewResult reviewResult = gApi.changes().id(changeId).current().review(reviewInput);
+ assertThat(reviewResult.error).isNull();
+
+ // user is removed as a reviewer, user2 is added as a CC by doing the post review request that
+ // removed user as a reviewer
+ assertThat(gApi.changes().id(changeId).get().reviewers.get(ReviewerState.REVIEWER)).isNull();
+ reviewerInfo =
+ Iterables.getOnlyElement(gApi.changes().id(changeId).get().reviewers.get(ReviewerState.CC));
+ assertThat(reviewerInfo._accountId).isEqualTo(user2.id().get());
+ }
+
+ @Test
public void removeReviewerNotPermitted() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
@@ -3016,7 +2408,68 @@
.id(r.getChangeId())
.reviewer(admin.id().toString())
.deleteVote(LabelId.CODE_REVIEW));
- assertThat(thrown).hasMessageThat().contains("delete vote not permitted");
+ assertThat(thrown).hasMessageThat().contains("Delete vote not permitted");
+ }
+
+ @Test
+ public void deleteVoteAlwaysPermittedForSelfVotes() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/heads/*").group(REGISTERED_USERS))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
+
+ gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(user.id().toString())
+ .deleteVote(LabelId.CODE_REVIEW);
+ }
+
+ @Test
+ public void deleteVoteAlwaysPermittedForAdmin() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/heads/*").group(REGISTERED_USERS))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
+
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(user.id().toString())
+ .deleteVote(LabelId.CODE_REVIEW);
}
@Test
@@ -3312,6 +2765,40 @@
}
@Test
+ public void queryChangesDefaultFieldMatchesOwner() throws Exception {
+ // We have to create a new user since changes are not deleted between tests, which means
+ // querying the standard users will lead to dirty results.
+ TestAccount changeOwner = accountCreator.createValid("changeOwner");
+ requestScopeOperations.setApiUser(changeOwner.id());
+ // Creating a change through the API since PushOneCommit changes are always owned by admin().
+ ChangeInput in = new ChangeInput();
+ in.branch = Constants.MASTER;
+ in.subject = "subject";
+ in.project = project.get();
+ ChangeInfo info = gApi.changes().createAsInfo(in);
+ assertThat(info.owner._accountId).isEqualTo(changeOwner.id().get());
+ requestScopeOperations.setApiUser(user.id());
+ List<ChangeInfo> results = query(changeOwner.email());
+ assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(info.changeId);
+ }
+
+ @Test
+ public void queryChangesDefaultFieldMatchesReviewer() throws Exception {
+ requestScopeOperations.setApiUser(admin.id());
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ // We have to create a new user since changes are not deleted between tests, which means
+ // querying the standard users will lead to dirty results.
+ TestAccount changeReviewer = accountCreator.createValid("changeReviewer");
+ gApi.changes().id(r.getChangeId()).addReviewer(changeReviewer.email());
+ requestScopeOperations.setApiUser(user.id());
+ List<ChangeInfo> results = query(changeReviewer.email());
+ assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r.getChangeId());
+ }
+
+ @Test
public void checkReviewedFlagBeforeAndAfterReview() throws Exception {
PushOneCommit.Result r = createChange();
ReviewerInput in = new ReviewerInput();
@@ -3572,7 +3059,11 @@
.review(ReviewInput.approve());
gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
- createChange();
+ PushOneCommit.Result change = createChange();
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
requestScopeOperations.setApiUser(user.id());
try (AutoCloseable ignored = disableNoteDb()) {
@@ -3587,6 +3078,34 @@
}
@Test
+ public void nonLazyloadQueryOptionsDoNotTouchDatabase() throws Exception {
+ requestScopeOperations.setApiUser(admin.id());
+ PushOneCommit.Result r1 = createChange();
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ PushOneCommit.Result change = createChange();
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
+
+ requestScopeOperations.setApiUser(user.id());
+ try (AutoCloseable ignored = disableNoteDb()) {
+ assertThat(
+ gApi.changes()
+ .query()
+ .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
+ .withOptions(EnumSet.complementOf(EnumSet.copyOf(ChangeJson.REQUIRE_LAZY_LOAD)))
+ .get())
+ .hasSize(2);
+ }
+ }
+
+ @Test
public void votable() throws Exception {
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
@@ -3851,6 +3370,7 @@
assertThat(change.status).isEqualTo(ChangeStatus.NEW);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
+ assertThat(change.removableLabels).isEmpty();
// add new label and assert that it's returned for existing changes
AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
@@ -3880,6 +3400,9 @@
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(new ReviewInput().label(verified.getName(), verified.getMax().getValue()));
+ change = gApi.changes().id(r.getChangeId()).get();
+ assertPermitted(change, LabelId.VERIFIED, -1, 0, 1);
+ assertOnlyRemovableLabel(change, LabelId.VERIFIED, "+1", admin);
try (ProjectConfigUpdate u = updateProject(project)) {
// remove label and assert that it's no longer returned for existing
@@ -3899,6 +3422,7 @@
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
+ assertThat(change.removableLabels).isEmpty();
// abandon the change and see that the returned labels stay the same
// while all permitted labels disappear.
@@ -3907,6 +3431,7 @@
assertThat(change.status).isEqualTo(ChangeStatus.ABANDONED);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels).isEmpty();
+ assertThat(change.removableLabels).isEmpty();
}
@Test
@@ -4023,6 +3548,7 @@
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
assertPermitted(change, LabelId.VERIFIED, 1);
+ assertThat(change.removableLabels).isEmpty();
// remove label and assert that it's no longer returned for existing
// changes, even if there is an approval for it
@@ -4040,6 +3566,7 @@
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
+ assertThat(change.removableLabels).isEmpty();
}
@Test
@@ -4136,6 +3663,7 @@
.containsExactly(LabelId.CODE_REVIEW, "Non-Author-Code-Review");
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2);
+ assertThat(change.removableLabels).isEmpty();
}
@Test
@@ -4151,6 +3679,7 @@
assertThat(change.submissionId).isNotNull();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2);
+ assertThat(change.removableLabels).isEmpty();
}
@Test
@@ -4857,8 +4386,12 @@
ListChangesOption.SKIP_DIFFSTAT);
PushOneCommit.Result change = createChange();
- int number = gApi.changes().id(change.getChangeId()).get()._number;
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
+ int number = gApi.changes().id(change.getChangeId()).get()._number;
try (AutoCloseable ignored = changeIndexOperations.disableReadsAndWrites()) {
assertThat(gApi.changes().id(project.get(), number).get(options).changeId)
.isEqualTo(change.getChangeId());
@@ -5030,19 +4563,6 @@
void call(String changeId, String reviewer) throws RestApiException;
}
- private static class TestWorkInProgressStateChangedListener
- implements WorkInProgressStateChangedListener {
- boolean invoked;
- Boolean wip;
-
- @Override
- public void onWorkInProgressStateChanged(WorkInProgressStateChangedListener.Event event) {
- this.invoked = true;
- this.wip =
- event.getChange().workInProgress != null ? event.getChange().workInProgress : false;
- }
- }
-
public static class TestAttentionSetListenerModule extends AbstractModule {
@Override
public void configure() {
@@ -5067,15 +4587,4 @@
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
-
- private static class TestCommitValidationListener implements CommitValidationListener {
- public CommitReceivedEvent receiveEvent;
-
- @Override
- public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
- throws CommitValidationException {
- this.receiveEvent = receiveEvent;
- return ImmutableList.of();
- }
- }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
new file mode 100644
index 0000000..522013e
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -0,0 +1,1142 @@
+// Copyright (C) 2022 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.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.extensions.api.changes.AttentionSetInput;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.RebaseChainInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.inject.Inject;
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ RebaseIT.RebaseViaRevisionApi.class, //
+ RebaseIT.RebaseViaChangeApi.class, //
+ RebaseIT.RebaseChain.class, //
+})
+public class RebaseIT {
+ public abstract static class Base extends AbstractDaemonTest {
+ @Inject protected RequestScopeOperations requestScopeOperations;
+ @Inject protected ProjectOperations projectOperations;
+ @Inject protected ExtensionRegistry extensionRegistry;
+
+ @FunctionalInterface
+ protected interface RebaseCall {
+ void call(String id) throws RestApiException;
+ }
+
+ @FunctionalInterface
+ protected interface RebaseCallWithInput {
+ void call(String id, RebaseInput in) throws RestApiException;
+ }
+
+ protected RebaseCall rebaseCall;
+ protected RebaseCallWithInput rebaseCallWithInput;
+
+ protected void init(RebaseCall call, RebaseCallWithInput callWithInput) {
+ this.rebaseCall = call;
+ this.rebaseCallWithInput = callWithInput;
+ }
+
+ @Test
+ public void rebaseChange() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Add an approval whose score should be copied on trivial rebase
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+
+ // Rebase the second change
+ rebaseCall.call(r2.getChangeId());
+
+ verifyRebaseForChange(r2.getChange().getId(), r.getCommit().name(), true, 2);
+
+ // Rebasing the second change again should fail
+ verifyChangeIsUpToDate(r2);
+ }
+
+ @Test
+ public void rebaseAbandonedChange() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo info = info(changeId);
+ assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+
+ ResourceConflictException thrown =
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(changeId));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Change " + r.getChange().getId() + " is abandoned");
+ }
+
+ @Test
+ public void rebaseOntoAbandonedChange() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Abandon the first change
+ String changeId = r.getChangeId();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo info = info(changeId);
+ assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+
+ RebaseInput ri = new RebaseInput();
+ ri.base = r.getCommit().name();
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r2.getChangeId(), ri));
+ assertThat(thrown).hasMessageThat().contains("base change is abandoned: " + changeId);
+ }
+
+ @Test
+ public void rebaseOntoSelf() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String commit = r.getCommit().name();
+ RebaseInput ri = new RebaseInput();
+ ri.base = commit;
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class, () -> rebaseCallWithInput.call(changeId, ri));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("cannot rebase change " + r.getChange().getId() + " onto itself");
+ }
+
+ @Test
+ public void rebaseChangeBaseRecursion() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+
+ RebaseInput ri = new RebaseInput();
+ ri.base = r2.getCommit().name();
+ String expectedMessage =
+ "base change "
+ + r2.getChangeId()
+ + " is a descendant of the current change - recursion not allowed";
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r1.getChangeId(), ri));
+ assertThat(thrown).hasMessageThat().contains(expectedMessage);
+ }
+
+ @Test
+ public void cannotRebaseOntoBaseThatIsNotPresentInTargetBranch() throws Exception {
+ ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+
+ BranchInput branchInput = new BranchInput();
+ branchInput.revision = initial.getName();
+ gApi.projects().name(project.get()).branch("foo").create(branchInput);
+
+ PushOneCommit.Result r1 =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "Change on foo branch", "a.txt", "a-content")
+ .to("refs/for/foo");
+ approve(r1.getChangeId());
+ gApi.changes().id(r1.getChangeId()).current().submit();
+
+ // reset HEAD in order to create a sibling of the first change
+ testRepo.reset(initial);
+
+ PushOneCommit.Result r2 =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "Change on master branch", "b.txt", "b-content")
+ .to("refs/for/master");
+
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.base = r1.getCommit().getName();
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r2.getChangeId(), rebaseInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "base change is targeting wrong branch: %s,refs/heads/foo", project.get()));
+
+ rebaseInput.base = "refs/heads/foo";
+ thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r2.getChangeId(), rebaseInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "base revision is missing from the destination branch: %s", rebaseInput.base));
+ }
+
+ @Test
+ public void rebaseUpToDateChange() throws Exception {
+ PushOneCommit.Result r = createChange();
+ verifyChangeIsUpToDate(r);
+ }
+
+ @Test
+ public void rebaseDoesNotAddWorkInProgress() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ // create an unrelated change so that we can rebase
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result unrelated = createChange();
+ gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(unrelated.getChangeId()).current().submit();
+
+ rebaseCall.call(r.getChangeId());
+
+ // change is still ready for review after rebase
+ assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isNull();
+ }
+
+ @Test
+ public void rebaseDoesNotRemoveWorkInProgress() throws Exception {
+ PushOneCommit.Result r = createChange();
+ change(r).setWorkInProgress();
+
+ // create an unrelated change so that we can rebase
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result unrelated = createChange();
+ gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(unrelated.getChangeId()).current().submit();
+
+ rebaseCall.call(r.getChangeId());
+
+ // change is still work in progress after rebase
+ assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isTrue();
+ }
+
+ @Test
+ public void rebaseAsUploaderInAttentionSet() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ TestAccount admin2 = accountCreator.admin2();
+ requestScopeOperations.setApiUser(admin2.id());
+ amendChangeWithUploader(r2, project, admin2);
+ gApi.changes()
+ .id(r2.getChangeId())
+ .addToAttentionSet(new AttentionSetInput(admin2.id().toString(), "manual update"));
+
+ rebaseCall.call(r2.getChangeId());
+ }
+
+ @Test
+ public void rebaseOnChangeNumber() throws Exception {
+ String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
+ PushOneCommit.Result r1 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
+ assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
+
+ Change.Id id1 = r1.getChange().getId();
+ RebaseInput in = new RebaseInput();
+ in.base = id1.toString();
+ rebaseCallWithInput.call(r2.getChangeId(), in);
+
+ Change.Id id2 = r2.getChange().getId();
+ ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ ri2 = ci2.revisions.get(ci2.currentRevision);
+ assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+ List<RelatedChangeAndCommitInfo> related =
+ gApi.changes().id(id2.get()).revision(ri2._number).related().changes;
+ assertThat(related).hasSize(2);
+ assertThat(related.get(0)._changeNumber).isEqualTo(id2.get());
+ assertThat(related.get(0)._revisionNumber).isEqualTo(2);
+ assertThat(related.get(1)._changeNumber).isEqualTo(id1.get());
+ assertThat(related.get(1)._revisionNumber).isEqualTo(1);
+ }
+
+ @Test
+ public void rebaseOnClosedChange() throws Exception {
+ String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
+ PushOneCommit.Result r1 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
+ assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
+
+ // Submit first change.
+ Change.Id id1 = r1.getChange().getId();
+ gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(id1.get()).current().submit();
+
+ // Rebase second change on first change.
+ RebaseInput in = new RebaseInput();
+ in.base = id1.toString();
+ rebaseCallWithInput.call(r2.getChangeId(), in);
+
+ Change.Id id2 = r2.getChange().getId();
+ ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ ri2 = ci2.revisions.get(ci2.currentRevision);
+ assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+ assertThat(gApi.changes().id(id2.get()).revision(ri2._number).related().changes).isEmpty();
+ }
+
+ @Test
+ public void rebaseOnNonExistingChange() throws Exception {
+ String changeId = createChange().getChangeId();
+ RebaseInput in = new RebaseInput();
+ in.base = "999999";
+ UnprocessableEntityException exception =
+ assertThrows(
+ UnprocessableEntityException.class, () -> rebaseCallWithInput.call(changeId, in));
+ assertThat(exception).hasMessageThat().contains("Base change not found: " + in.base);
+ }
+
+ @Test
+ public void rebaseNotAllowedWithoutPermission() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase the second
+ String changeId = r2.getChangeId();
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown = assertThrows(AuthException.class, () -> rebaseCall.call(changeId));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "rebase not permitted (change owners and users with the 'Submit' or 'Rebase'"
+ + " permission can rebase if they have the 'Push' permission)");
+ }
+
+ @Test
+ public void rebaseAllowedWithPermission() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // Rebase the second
+ String changeId = r2.getChangeId();
+ requestScopeOperations.setApiUser(user.id());
+ rebaseCall.call(changeId);
+ }
+
+ @Test
+ public void rebaseNotAllowedWithoutPushPermission() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+ .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+ .update();
+
+ // Rebase the second
+ String changeId = r2.getChangeId();
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown = assertThrows(AuthException.class, () -> rebaseCall.call(changeId));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "rebase not permitted (change owners and users with the 'Submit' or 'Rebase'"
+ + " permission can rebase if they have the 'Push' permission)");
+ }
+
+ @Test
+ public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+ .update();
+
+ // Rebase the second
+ String changeId = r2.getChangeId();
+ AuthException thrown = assertThrows(AuthException.class, () -> rebaseCall.call(changeId));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "rebase not permitted (change owners and users with the 'Submit' or 'Rebase'"
+ + " permission can rebase if they have the 'Push' permission)");
+ }
+
+ @Test
+ public void rebaseWithValidationOptions() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.validationOptions = ImmutableMap.of("key", "value");
+
+ TestCommitValidationListener testCommitValidationListener =
+ new TestCommitValidationListener();
+ try (ExtensionRegistry.Registration unusedRegistration =
+ extensionRegistry.newRegistration().add(testCommitValidationListener)) {
+ // Rebase the second change
+ rebaseCallWithInput.call(r2.getChangeId(), rebaseInput);
+ assertThat(testCommitValidationListener.receiveEvent.pushOptions)
+ .containsExactly("key", "value");
+ }
+ }
+
+ protected void verifyRebaseForChange(
+ Change.Id changeId, Change.Id baseChangeId, boolean shouldHaveApproval)
+ throws RestApiException {
+ verifyRebaseForChange(changeId, baseChangeId, shouldHaveApproval, 2);
+ }
+
+ protected void verifyRebaseForChange(
+ Change.Id changeId,
+ Change.Id baseChangeId,
+ boolean shouldHaveApproval,
+ int expectedNumRevisions)
+ throws RestApiException {
+ ChangeInfo baseInfo = gApi.changes().id(baseChangeId.get()).get(CURRENT_REVISION);
+ verifyRebaseForChange(
+ changeId, baseInfo.currentRevision, shouldHaveApproval, expectedNumRevisions);
+ }
+
+ protected void verifyRebaseForChange(
+ Change.Id changeId, String baseCommit, boolean shouldHaveApproval, int expectedNumRevisions)
+ throws RestApiException {
+ ChangeInfo info =
+ gApi.changes().id(changeId.get()).get(CURRENT_REVISION, CURRENT_COMMIT, DETAILED_LABELS);
+
+ RevisionInfo r = info.revisions.get(info.currentRevision);
+ assertThat(r._number).isEqualTo(expectedNumRevisions);
+
+ // ...and the base should be correct
+ assertThat(r.commit.parents).hasSize(1);
+ assertWithMessage("base commit for change " + changeId)
+ .that(r.commit.parents.get(0).commit)
+ .isEqualTo(baseCommit);
+
+ // ...and the committer and description should be correct
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo(admin.fullName());
+ assertThat(committer.email).isEqualTo(admin.email());
+ String description = info.revisions.get(info.currentRevision).description;
+ assertThat(description).isEqualTo("Rebase");
+
+ if (shouldHaveApproval) {
+ // ...and the approval was copied
+ LabelInfo cr = info.labels.get(LabelId.CODE_REVIEW);
+ assertThat(cr).isNotNull();
+ assertThat(cr.all).isNotNull();
+ assertThat(cr.all).hasSize(1);
+ assertThat(cr.all.get(0).value).isEqualTo(1);
+ }
+ }
+
+ protected void verifyChangeIsUpToDate(PushOneCommit.Result r) {
+ ResourceConflictException thrown =
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(r.getChangeId()));
+ assertThat(thrown).hasMessageThat().contains("Change is already up to date");
+ }
+
+ protected static class TestCommitValidationListener implements CommitValidationListener {
+ public CommitReceivedEvent receiveEvent;
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ this.receiveEvent = receiveEvent;
+ return ImmutableList.of();
+ }
+ }
+
+ protected static class TestWorkInProgressStateChangedListener
+ implements WorkInProgressStateChangedListener {
+ boolean invoked;
+ Boolean wip;
+
+ @Override
+ public void onWorkInProgressStateChanged(WorkInProgressStateChangedListener.Event event) {
+ this.invoked = true;
+ this.wip =
+ event.getChange().workInProgress != null ? event.getChange().workInProgress : false;
+ }
+ }
+ }
+
+ public abstract static class Rebase extends Base {
+ @Test
+ public void rebaseChangeBase() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange();
+ RebaseInput ri = new RebaseInput();
+
+ // rebase r3 directly onto master (break dep. towards r2)
+ ri.base = "";
+ rebaseCallWithInput.call(r3.getChangeId(), ri);
+ PatchSet ps3 = r3.getPatchSet();
+ assertThat(ps3.id().get()).isEqualTo(2);
+
+ // rebase r2 onto r3 (referenced by ref)
+ ri.base = ps3.id().toRefName();
+ rebaseCallWithInput.call(r2.getChangeId(), ri);
+ PatchSet ps2 = r2.getPatchSet();
+ assertThat(ps2.id().get()).isEqualTo(2);
+
+ // rebase r1 onto r2 (referenced by commit)
+ ri.base = ps2.commitId().name();
+ rebaseCallWithInput.call(r1.getChangeId(), ri);
+ PatchSet ps1 = r1.getPatchSet();
+ assertThat(ps1.id().get()).isEqualTo(2);
+
+ // rebase r1 onto r3 (referenced by change number)
+ ri.base = String.valueOf(r3.getChange().getId().get());
+ rebaseCallWithInput.call(r1.getChangeId(), ri);
+ assertThat(r1.getPatchSetId().get()).isEqualTo(3);
+ }
+
+ @Test
+ public void rebaseWithConflict_conflictsAllowed() throws Exception {
+ String patchSetSubject = "patch set change";
+ String patchSetContent = "patch set content";
+ String baseSubject = "base change";
+ String baseContent = "base content";
+
+ PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ testRepo.reset("HEAD~1");
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ patchSetSubject,
+ PushOneCommit.FILE_NAME,
+ patchSetContent);
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+
+ String changeId = r2.getChangeId();
+ RevCommit patchSet = r2.getCommit();
+ RevCommit base = r1.getCommit();
+
+ TestWorkInProgressStateChangedListener wipStateChangedListener =
+ new TestWorkInProgressStateChangedListener();
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry.newRegistration().add(wipStateChangedListener)) {
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.allowConflicts = true;
+ ChangeInfo changeInfo =
+ gApi.changes().id(changeId).revision(patchSet.name()).rebaseAsInfo(rebaseInput);
+ assertThat(changeInfo.containsGitConflicts).isTrue();
+ assertThat(changeInfo.workInProgress).isTrue();
+ }
+ assertThat(wipStateChangedListener.invoked).isTrue();
+ assertThat(wipStateChangedListener.wip).isTrue();
+
+ // To get the revisions, we must retrieve the change with more change options.
+ ChangeInfo changeInfo =
+ gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
+ assertThat(changeInfo.revisions).hasSize(2);
+ assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
+ .isEqualTo(base.name());
+
+ // Verify that the file content in the created patch set is correct.
+ // We expect that it has conflict markers to indicate the conflict.
+ BinaryResult bin =
+ gApi.changes().id(changeId).current().file(PushOneCommit.FILE_NAME).content();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bin.writeTo(os);
+ String fileContent = new String(os.toByteArray(), UTF_8);
+ String patchSetSha1 = abbreviateName(patchSet, 6);
+ String baseSha1 = abbreviateName(base, 6);
+ assertThat(fileContent)
+ .isEqualTo(
+ "<<<<<<< PATCH SET ("
+ + patchSetSha1
+ + " "
+ + patchSetSubject
+ + ")\n"
+ + patchSetContent
+ + "\n"
+ + "=======\n"
+ + baseContent
+ + "\n"
+ + ">>>>>>> BASE ("
+ + baseSha1
+ + " "
+ + baseSubject
+ + ")\n");
+
+ // Verify the message that has been posted on the change.
+ List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
+ assertThat(messages).hasSize(2);
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ "Patch Set 2: Patch Set 1 was rebased\n\n"
+ + "The following files contain Git conflicts:\n"
+ + "* "
+ + PushOneCommit.FILE_NAME
+ + "\n");
+ }
+
+ @Test
+ public void rebaseWithConflict_conflictsForbidden() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ PushOneCommit.FILE_NAME,
+ "other content",
+ "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5");
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+ ResourceConflictException exception =
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(r2.getChangeId()));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "Change %s could not be rebased due to a conflict during merge.\n\n"
+ + "merge conflict(s):\n%s",
+ r2.getChange().getId(), PushOneCommit.FILE_NAME));
+ }
+
+ @Test
+ public void rebaseFromRelationChainToClosedChange() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ testRepo.reset("HEAD~1");
+
+ createChange();
+ PushOneCommit.Result r3 = createChange();
+
+ // Submit first change.
+ Change.Id id1 = r1.getChange().getId();
+ gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(id1.get()).current().submit();
+
+ // Rebase third change on first change.
+ RebaseInput in = new RebaseInput();
+ in.base = id1.toString();
+ rebaseCallWithInput.call(r3.getChangeId(), in);
+
+ Change.Id id3 = r3.getChange().getId();
+ ChangeInfo ci3 = get(r3.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ RevisionInfo ri3 = ci3.revisions.get(ci3.currentRevision);
+ assertThat(ri3.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+ assertThat(gApi.changes().id(id3.get()).revision(ri3._number).related().changes).isEmpty();
+ }
+ }
+
+ public static class RebaseViaRevisionApi extends Rebase {
+ @Before
+ public void setUp() throws Exception {
+ init(
+ id -> gApi.changes().id(id).current().rebase(),
+ (id, in) -> gApi.changes().id(id).current().rebase(in));
+ }
+
+ @Test
+ public void rebaseOutdatedPatchSet() throws Exception {
+ String fileName1 = "a.txt";
+ String fileContent1 = "some content";
+ String fileName2 = "b.txt";
+ String fileContent2Ps1 = "foo";
+ String fileContent2Ps2 = "foo/bar";
+
+ // Create two changes both with the same parent touching disjunct files
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName1, fileContent1)
+ .to("refs/for/master");
+ r.assertOkStatus();
+ String changeId1 = r.getChangeId();
+ testRepo.reset("HEAD~1");
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName2, fileContent2Ps1);
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+ String changeId2 = r2.getChangeId();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(changeId1).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Amend the second change so that it has 2 patch sets
+ amendChange(
+ changeId2,
+ "refs/for/master",
+ admin,
+ testRepo,
+ PushOneCommit.SUBJECT,
+ fileName2,
+ fileContent2Ps2)
+ .assertOkStatus();
+ ChangeInfo changeInfo2 = gApi.changes().id(changeId2).get();
+ assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(2);
+
+ // Rebase the first patch set of the second change
+ gApi.changes().id(changeId2).revision(1).rebase();
+
+ // Second change should have 3 patch sets
+ changeInfo2 = gApi.changes().id(changeId2).get();
+ assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(3);
+
+ // ... and the committer and description should be correct
+ ChangeInfo info = gApi.changes().id(changeId2).get(CURRENT_REVISION, CURRENT_COMMIT);
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo(admin.fullName());
+ assertThat(committer.email).isEqualTo(admin.email());
+ String description = info.revisions.get(info.currentRevision).description;
+ assertThat(description).isEqualTo("Rebase");
+
+ // ... and the file contents should match with patch set 1 based on change1
+ assertThat(gApi.changes().id(changeId2).current().file(fileName1).content().asString())
+ .isEqualTo(fileContent1);
+ assertThat(gApi.changes().id(changeId2).current().file(fileName2).content().asString())
+ .isEqualTo(fileContent2Ps1);
+ }
+ }
+
+ public static class RebaseViaChangeApi extends Rebase {
+ @Before
+ public void setUp() throws Exception {
+ init(id -> gApi.changes().id(id).rebase(), (id, in) -> gApi.changes().id(id).rebase(in));
+ }
+ }
+
+ public static class RebaseChain extends Base {
+ @Before
+ public void setUp() throws Exception {
+ init(
+ id -> {
+ @SuppressWarnings("unused")
+ Object unused = gApi.changes().id(id).rebaseChain();
+ },
+ (id, in) -> {
+ @SuppressWarnings("unused")
+ Object unused = gApi.changes().id(id).rebaseChain(in);
+ });
+ }
+
+ @Override
+ protected void verifyChangeIsUpToDate(PushOneCommit.Result r) {
+ ResourceConflictException thrown =
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(r.getChangeId()));
+ assertThat(thrown).hasMessageThat().contains("The whole chain is already up to date.");
+ }
+
+ @Test
+ public void rebaseChain() throws Exception {
+ // Create changes with the following hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+ // * r3
+ // * r4
+ // *r5
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange();
+ PushOneCommit.Result r4 = createChange();
+ PushOneCommit.Result r5 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Add an approval whose score should be copied on trivial rebase
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+ gApi.changes().id(r3.getChangeId()).current().review(ReviewInput.recommend());
+
+ // Rebase the chain through r4.
+ verifyRebaseChainResponse(
+ gApi.changes().id(r4.getChangeId()).rebaseChain(), false, r2, r3, r4);
+
+ // Only r2, r3 and r4 are rebased.
+ verifyRebaseForChange(r2.getChange().getId(), r.getCommit().name(), true, 2);
+ verifyRebaseForChange(r3.getChange().getId(), r2.getChange().getId(), true);
+ verifyRebaseForChange(r4.getChange().getId(), r3.getChange().getId(), false);
+
+ verifyChangeIsUpToDate(r2);
+ verifyChangeIsUpToDate(r3);
+ verifyChangeIsUpToDate(r4);
+
+ // r5 wasn't rebased.
+ ChangeInfo r5info = gApi.changes().id(r5.getChangeId()).get(CURRENT_REVISION);
+ assertThat(r5info.revisions.get(r5info.currentRevision)._number).isEqualTo(1);
+
+ // Rebasing r5
+ verifyRebaseChainResponse(
+ gApi.changes().id(r5.getChangeId()).rebaseChain(), false, r2, r3, r4, r5);
+
+ verifyRebaseForChange(r5.getChange().getId(), r4.getChange().getId(), false);
+ }
+
+ @Test
+ public void rebasePartlyOutdatedChain() throws Exception {
+ final String file = "modified_file.txt";
+ final String oldContent = "old content";
+ final String newContent = "new content";
+ // Create changes with the following revision hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+ // * r3/1 r3/2
+ // * r4
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange("original patch-set", file, oldContent);
+ PushOneCommit.Result r4 = createChange();
+ gApi.changes()
+ .id(r3.getChangeId())
+ .edit()
+ .modifyFile(file, RawInputUtil.create(newContent.getBytes(UTF_8)));
+ gApi.changes().id(r3.getChangeId()).edit().publish();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase the chain through r4.
+ rebaseCall.call(r4.getChangeId());
+
+ verifyRebaseForChange(r2.getChange().getId(), r.getCommit().name(), false, 2);
+ verifyRebaseForChange(r3.getChange().getId(), r2.getChange().getId(), false, 3);
+ verifyRebaseForChange(r4.getChange().getId(), r3.getChange().getId(), false);
+
+ assertThat(gApi.changes().id(r3.getChangeId()).current().file(file).content().asString())
+ .isEqualTo(newContent);
+
+ verifyChangeIsUpToDate(r2);
+ verifyChangeIsUpToDate(r3);
+ verifyChangeIsUpToDate(r4);
+ }
+
+ @Test
+ public void rebaseChainWithConflicts_conflictsForbidden() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ PushOneCommit.FILE_NAME,
+ "other content",
+ "I0020020020020020020020020020020020020002");
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+ PushOneCommit.Result r3 = createChange("refs/for/master");
+ r3.assertOkStatus();
+ ResourceConflictException exception =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(r3.getChangeId()).rebaseChain());
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "Change %s could not be rebased due to a conflict during merge.\n\n"
+ + "merge conflict(s):\n%s",
+ r2.getChange().getId(), PushOneCommit.FILE_NAME));
+ }
+
+ @Test
+ public void rebaseChainWithConflicts_conflictsAllowed() throws Exception {
+ String patchSetSubject = "patch set change";
+ String patchSetContent = "patch set content";
+ String baseSubject = "base change";
+ String baseContent = "base content";
+
+ PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ testRepo.reset("HEAD~1");
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ patchSetSubject,
+ PushOneCommit.FILE_NAME,
+ patchSetContent);
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+
+ String changeWithConflictId = r2.getChangeId();
+ RevCommit patchSet = r2.getCommit();
+ RevCommit base = r1.getCommit();
+ PushOneCommit.Result r3 = createChange("refs/for/master");
+ r3.assertOkStatus();
+
+ TestWorkInProgressStateChangedListener wipStateChangedListener =
+ new TestWorkInProgressStateChangedListener();
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry.newRegistration().add(wipStateChangedListener)) {
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.allowConflicts = true;
+ Response<RebaseChainInfo> res =
+ gApi.changes().id(r3.getChangeId()).rebaseChain(rebaseInput);
+ verifyRebaseChainResponse(res, true, r2, r3);
+ RebaseChainInfo rebaseChainInfo = res.value();
+ ChangeInfo changeWithConflictInfo = rebaseChainInfo.rebasedChanges.get(0);
+ assertThat(changeWithConflictInfo.changeId).isEqualTo(r2.getChangeId());
+ assertThat(changeWithConflictInfo.containsGitConflicts).isTrue();
+ assertThat(changeWithConflictInfo.workInProgress).isTrue();
+ ChangeInfo childChangeInfo = rebaseChainInfo.rebasedChanges.get(1);
+ assertThat(childChangeInfo.changeId).isEqualTo(r3.getChangeId());
+ assertThat(childChangeInfo.containsGitConflicts).isTrue();
+ assertThat(childChangeInfo.workInProgress).isTrue();
+ }
+ assertThat(wipStateChangedListener.invoked).isTrue();
+ assertThat(wipStateChangedListener.wip).isTrue();
+
+ // To get the revisions, we must retrieve the change with more change options.
+ ChangeInfo changeInfo =
+ gApi.changes()
+ .id(changeWithConflictId)
+ .get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
+ assertThat(changeInfo.revisions).hasSize(2);
+ assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
+ .isEqualTo(base.name());
+
+ // Verify that the file content in the created patch set is correct.
+ // We expect that it has conflict markers to indicate the conflict.
+ BinaryResult bin =
+ gApi.changes().id(changeWithConflictId).current().file(PushOneCommit.FILE_NAME).content();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bin.writeTo(os);
+ String fileContent = new String(os.toByteArray(), UTF_8);
+ String patchSetSha1 = abbreviateName(patchSet, 6);
+ String baseSha1 = abbreviateName(base, 6);
+ assertThat(fileContent)
+ .isEqualTo(
+ "<<<<<<< PATCH SET ("
+ + patchSetSha1
+ + " "
+ + patchSetSubject
+ + ")\n"
+ + patchSetContent
+ + "\n"
+ + "=======\n"
+ + baseContent
+ + "\n"
+ + ">>>>>>> BASE ("
+ + baseSha1
+ + " "
+ + baseSubject
+ + ")\n");
+
+ // Verify the message that has been posted on the change.
+ List<ChangeMessageInfo> messages = gApi.changes().id(changeWithConflictId).messages();
+ assertThat(messages).hasSize(2);
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ "Patch Set 2: Patch Set 1 was rebased\n\n"
+ + "The following files contain Git conflicts:\n"
+ + "* "
+ + PushOneCommit.FILE_NAME
+ + "\n");
+ }
+
+ @Test
+ public void rebaseOntoMidChain() throws Exception {
+ // Create changes with the following hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+ // * r3
+ // * r4
+ PushOneCommit.Result r = createChange();
+ r.assertOkStatus();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ r2.assertOkStatus();
+ PushOneCommit.Result r3 = createChange();
+ r3.assertOkStatus();
+ PushOneCommit.Result r4 = createChange();
+
+ RebaseInput ri = new RebaseInput();
+ ri.base = r3.getCommit().name();
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r4.getChangeId(), ri));
+ assertThat(thrown).hasMessageThat().contains("recursion not allowed");
+ }
+
+ private void verifyRebaseChainResponse(
+ Response<RebaseChainInfo> res,
+ boolean shouldHaveConflicts,
+ PushOneCommit.Result... changes) {
+ assertThat(res.statusCode()).isEqualTo(200);
+ RebaseChainInfo info = res.value();
+ assertThat(info.rebasedChanges.stream().map(c -> c._number).collect(Collectors.toList()))
+ .containsExactlyElementsIn(
+ Arrays.stream(changes)
+ .map(c -> c.getChange().getId().get())
+ .collect(Collectors.toList()))
+ .inOrder();
+ assertThat(info.containsGitConflicts).isEqualTo(shouldHaveConflicts ? true : null);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index d630296..04bdf15 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -601,6 +601,20 @@
}
@Test
+ public void getGroupFromMetaId() throws Exception {
+ AccountGroup.UUID uuid = groupOperations.newGroup().create();
+ InternalGroup preUpdateState = groupCache.get(uuid).get();
+ gApi.groups().id(uuid.toString()).description("New description");
+
+ InternalGroup postUpdateState = groupCache.get(uuid).get();
+ assertThat(postUpdateState).isNotEqualTo(preUpdateState);
+ assertThat(groupCache.getFromMetaId(uuid, preUpdateState.getRefState()))
+ .isEqualTo(preUpdateState);
+ assertThat(groupCache.getFromMetaId(uuid, postUpdateState.getRefState()))
+ .isEqualTo(postUpdateState);
+ }
+
+ @Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void getSystemGroupByConfiguredName() throws Exception {
GroupReference anonymousUsersGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index a625a70..93f91dd 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -54,7 +54,7 @@
@Inject private IndexOperations.Project projectIndexOperations;
private static final ImmutableSet<String> FIELDS =
- ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
+ ImmutableSet.of(ProjectField.NAME_SPEC.getName(), ProjectField.REF_STATE.getName());
@Test
public void indexProject_indexesRefStateOfProjectAndParents() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
index 74bfe0f..9d37497 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -260,6 +261,96 @@
}
@Test
+ public void migrateBlockingLabel_withBranchAttribute() throws Exception {
+ createLabelWithBranch(
+ "Foo",
+ "MaxWithBlock",
+ /* ignoreSelfApproval= */ false,
+ ImmutableList.of("refs/heads/master"));
+
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\"",
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_withMultipleBranchAttributes() throws Exception {
+ createLabelWithBranch(
+ "Foo",
+ "MaxWithBlock",
+ /* ignoreSelfApproval= */ false,
+ ImmutableList.of("refs/heads/master", "refs/heads/develop"));
+
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\" "
+ + "OR branch:\\\"refs/heads/develop\\\"",
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_withRegexBranchAttribute() throws Exception {
+ createLabelWithBranch(
+ "Foo",
+ "MaxWithBlock",
+ /* ignoreSelfApproval= */ false,
+ ImmutableList.of("^refs/heads/main-.*"));
+
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "branch:\\\"^refs/heads/main-.*\\\"",
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_withRegexAndNonRegexBranchAttributes() throws Exception {
+ createLabelWithBranch(
+ "Foo",
+ "MaxWithBlock",
+ /* ignoreSelfApproval= */ false,
+ ImmutableList.of("refs/heads/master", "^refs/heads/main-.*"));
+
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\" "
+ + "OR branch:\\\"^refs/heads/main-.*\\\"",
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
public void migrationIsIdempotent() throws Exception {
String oldRefsConfigId;
try (Repository repo = repoManager.openRepository(project)) {
@@ -381,6 +472,21 @@
gApi.projects().name(project.get()).label(labelName).create(input);
}
+ private void createLabelWithBranch(
+ String labelName,
+ String function,
+ boolean ignoreSelfApproval,
+ ImmutableList<String> refPatterns)
+ throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = labelName;
+ input.function = function;
+ input.ignoreSelfApproval = ignoreSelfApproval;
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.branches = refPatterns;
+ gApi.projects().name(project.get()).label(labelName).create(input);
+ }
+
@CanIgnoreReturnValue
private SubmitRequirementApi createSubmitRequirement(
String name, String submitExpression, boolean canOverride) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 64e3762..9710bf4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -43,6 +43,7 @@
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.httpd.restapi.ParameterParser;
import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.validators.CommitValidationException;
@@ -801,7 +802,7 @@
@Test
@GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
- public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
+ public void autoRetryWithTrace() throws Exception {
String changeId = createChange().getChangeId();
approve(changeId);
@@ -811,6 +812,49 @@
RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).startsWith("retry-on-failure-");
+ assertThat(traceSubmitRule.traceId).startsWith("retry-on-failure-");
+ assertThat(traceSubmitRule.isLoggingForced).isTrue();
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
+ public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+ traceSubmitRule.failAlways = true;
+ try (Registration registration =
+ extensionRegistry
+ .newRegistration()
+ .add(traceSubmitRule)
+ .add(
+ new ExceptionHook() {
+ @Override
+ public boolean shouldRetry(String actionType, String actionName, Throwable t) {
+ return true;
+ }
+ })) {
+ RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+ assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(traceSubmitRule.traceId).isNull();
+ assertThat(traceSubmitRule.isLoggingForced).isFalse();
+ }
+ }
+
+ @Test
+ public void noAutoRetryWithTraceIfDisabled() throws Exception {
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+ traceSubmitRule.failOnce = true;
+ try (Registration registration = extensionRegistry.newRegistration().add(traceSubmitRule)) {
+ RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+ assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
assertThat(traceSubmitRule.traceId).isNull();
assertThat(traceSubmitRule.isLoggingForced).isFalse();
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index eb827c0..3531d1c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -20,6 +20,8 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
+import static com.google.gerrit.entities.RefNames.patchSetRef;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -28,11 +30,13 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.RestSession;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -43,8 +47,10 @@
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -55,6 +61,7 @@
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -71,8 +78,15 @@
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
+import java.io.File;
+import java.io.IOException;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ReflogEntry;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -105,28 +119,246 @@
}
@Test
+ @UseLocalDisk
public void voteOnBehalfOf() throws Exception {
allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ try (Repository repo = repoManager.openRepository(project)) {
+ String changeMetaRef = changeMetaRef(r.getChange().getId());
+ createRefLogFileIfMissing(repo, changeMetaRef);
+
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // The change meta commit is created by the server and has the impersonated user as the
+ // author.
+ // Person idents of users in NoteDb commits are obfuscated due to privacy reasons.
+ RevCommit changeMetaCommit = projectOperations.project(project).getHead(changeMetaRef);
+ assertThat(changeMetaCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(changeMetaCommit.getAuthorIdent().getEmailAddress())
+ .isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
+
+ // The ref log for the change meta ref records the impersonated user.
+ ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+ assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithOtherImpersonatedVote_sameValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount realUser2 = admin2;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
ReviewInput in = ReviewInput.recommend();
- in.onBehalfOf = user.id().toString();
+ in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Message on behalf of";
revision.review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
- assertThat(psa.accountId()).isEqualTo(user.id());
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
- assertThat(psa.realAccountId()).isEqualTo(admin.id());
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
ChangeData cd = r.getChange();
ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
assertThat(m.getMessage()).endsWith(in.message);
- assertThat(m.getAuthor()).isEqualTo(user.id());
- assertThat(m.getRealAuthor()).isEqualTo(admin.id());
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // realUser2 votes Code-Review+1 on behalf of impersonatedUser, this should override the
+ // impersonated Code-Review+1 of realUser with an impersonated Code-Review+1 of realUser2
+ requestScopeOperations.setApiUser(realUser2.id());
+ in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Another message on behalf of";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser2.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser2.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithOtherImpersonatedVote_differentValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount realUser2 = admin2;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // realUser2 votes Code-Review-1 on behalf of impersonatedUser, this should override the
+ // impersonated Code-Review+1 of realUser with an impersonated Code-Review-1 of realUser2
+ requestScopeOperations.setApiUser(realUser2.id());
+ in = ReviewInput.dislike();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Another message on behalf of";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(-1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser2.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser2.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithNonImpersonatedVote_sameValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // impersonatedUser votes Code-Review+1 themselves, this should override the impersonated
+ // Code-Review+1 with a non-impersonated Code-Review+1
+ requestScopeOperations.setApiUser(impersonatedUser.id());
+ in = ReviewInput.recommend();
+ in.message = "Message";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(impersonatedUser.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(impersonatedUser.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithNonImpersonatedVote_differentValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // impersonatedUser votes Code-Review-1 themselves, this should override the impersonated
+ // Code-Review+1 with a non-impersonated Code-Review-1
+ requestScopeOperations.setApiUser(impersonatedUser.id());
+ in = ReviewInput.dislike();
+ in.message = "Message";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(-1);
+ assertThat(psa.realAccountId()).isEqualTo(impersonatedUser.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(impersonatedUser.id());
}
@Test
@@ -342,21 +574,120 @@
}
@Test
- public void submitOnBehalfOf() throws Exception {
- allowSubmitOnBehalfOf();
- PushOneCommit.Result r = createChange();
+ @UseLocalDisk
+ public void submitOnBehalfOf_mergeAlways() throws Exception {
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = admin2;
+
+ // Create a project with MERGE_ALWAYS submit strategy so that a merge commit is created on
+ // submit and we can verify its committer and author and the ref log for the update of the
+ // target branch.
+ Project.NameKey project =
+ projectOperations.newProject().submitType(SubmitType.MERGE_ALWAYS).create();
+
+ testSubmitOnBehalfOf(project, realUser, impersonatedUser);
+
+ // The merge commit is created by the server and has the impersonated user as the author.
+ RevCommit mergeCommit = projectOperations.project(project).getHead("refs/heads/master");
+ assertThat(mergeCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(mergeCommit.getAuthorIdent().getEmailAddress()).isEqualTo(impersonatedUser.email());
+
+ // The ref log for the target branch records the impersonated user.
+ try (Repository repo = repoManager.openRepository(project)) {
+ ReflogEntry targetBranchRefLogEntry =
+ repo.getReflogReader("refs/heads/master").getLastEntry();
+ assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @Test
+ @UseLocalDisk
+ public void submitOnBehalfOf_rebaseAlways() throws Exception {
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = admin2;
+
+ // Create a project with REBASE_ALWAYS submit strategy so that a new patch set is created on
+ // submit and we can verify its committer and author and the ref log for the update of the
+ // patch set ref and the target branch.
+ Project.NameKey project =
+ projectOperations.newProject().submitType(SubmitType.REBASE_ALWAYS).create();
+
+ ChangeData cd = testSubmitOnBehalfOf(project, realUser, impersonatedUser);
+
+ // Rebase on submit is expected to create a new patch set.
+ assertThat(cd.currentPatchSet().id().get()).isEqualTo(2);
+
+ // The patch set commit is created by the impersonated user and has the real user as the author.
+ // Recording the real user as the author seems to a bug, we would expect the author to be the
+ // impersonated user.
+ RevCommit newPatchSetCommit =
+ projectOperations.project(project).getHead(cd.currentPatchSet().refName());
+ assertThat(newPatchSetCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ assertThat(newPatchSetCommit.getAuthorIdent().getEmailAddress()).isEqualTo(realUser.email());
+
+ try (Repository repo = repoManager.openRepository(project)) {
+ // The ref log for the patch set ref records the impersonated user.
+ ReflogEntry patchSetRefLogEntry =
+ repo.getReflogReader(cd.currentPatchSet().refName()).getLastEntry();
+ assertThat(patchSetRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+
+ // The ref log for the target branch records the impersonated user.
+ ReflogEntry targetBranchRefLogEntry =
+ repo.getReflogReader("refs/heads/master").getLastEntry();
+ assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @CanIgnoreReturnValue
+ private ChangeData testSubmitOnBehalfOf(
+ Project.NameKey project, TestAccount realUser, TestAccount impersonatedUser)
+ throws Exception {
+ allowSubmitOnBehalfOf(project);
+
+ TestRepository<InMemoryRepository> testRepo = cloneProject(project, realUser);
+
+ PushOneCommit.Result r = createChange(testRepo);
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
SubmitInput in = new SubmitInput();
- in.onBehalfOf = admin2.email();
- gApi.changes().id(changeId).current().submit(in);
+ in.onBehalfOf = impersonatedUser.email();
- ChangeData cd = r.getChange();
- assertThat(cd.change().isMerged()).isTrue();
- PatchSetApproval submitter =
- approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
- assertThat(submitter.accountId()).isEqualTo(admin2.id());
- assertThat(submitter.realAccountId()).isEqualTo(admin.id());
+ try (Repository repo = repoManager.openRepository(project)) {
+ String changeMetaRef = changeMetaRef(r.getChange().getId());
+ createRefLogFileIfMissing(repo, changeMetaRef);
+ createRefLogFileIfMissing(repo, "refs/heads/master");
+ createRefLogFileIfMissing(repo, patchSetRef(PatchSet.id(r.getChange().getId(), 2)));
+
+ gApi.changes().id(changeId).current().submit(in);
+
+ ChangeData cd = r.getChange();
+ assertThat(cd.change().isMerged()).isTrue();
+ PatchSetApproval submitter =
+ approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
+ assertThat(submitter.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(submitter.realAccountId()).isEqualTo(realUser.id());
+
+ // The change meta commit is created by the server and has the impersonated user as the
+ // author.
+ // Person idents of users in NoteDb commits are obfuscated due to privacy reasons.
+ RevCommit changeMetaCommit = projectOperations.project(project).getHead(changeMetaRef);
+ assertThat(changeMetaCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(changeMetaCommit.getAuthorIdent().getEmailAddress())
+ .isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
+
+ // The ref log for the change meta ref records the impersonated user.
+ ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+ assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+
+ return cd;
+ }
}
@Test
@@ -591,6 +922,10 @@
}
private void allowSubmitOnBehalfOf() throws Exception {
+ allowSubmitOnBehalfOf(project);
+ }
+
+ private void allowSubmitOnBehalfOf(Project.NameKey project) throws Exception {
String heads = "refs/heads/*";
projectOperations
.project(project)
@@ -630,4 +965,12 @@
private static Header runAsHeader(Object user) {
return new BasicHeader("X-Gerrit-RunAs", user.toString());
}
+
+ private void createRefLogFileIfMissing(Repository repo, String ref) throws IOException {
+ File log = new File(repo.getDirectory(), "logs/" + ref);
+ if (!log.exists()) {
+ log.getParentFile().mkdirs();
+ assertThat(log.createNewFile()).isTrue();
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index 81c098f..aeebc10 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -234,11 +234,11 @@
PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
submitWithConflict(
change2.getChangeId(),
- "Cannot rebase "
- + change2.getCommit().name()
- + ": The change could not be rebased due to a conflict during merge.\n\n"
- + "merge conflict(s):\n"
- + "a.txt");
+ String.format(
+ "Cannot rebase %s: Change %s could not be rebased due to a conflict during merge.\n\n"
+ + "merge conflict(s):\n"
+ + "a.txt",
+ change2.getCommit().name(), change2.getChange().getId()));
RevCommit head = projectOperations.project(project).getHead("master");
assertThat(head).isEqualTo(headAfterFirstSubmit);
assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
@@ -362,12 +362,11 @@
submitWithConflict(
change2.getChangeId(),
- "Cannot rebase "
- + change2.getCommit().getName()
- + ": "
- + "The change could not be rebased due to a conflict during merge.\n\n"
- + "merge conflict(s):\n"
- + "fileName 2");
+ String.format(
+ "Cannot rebase %s: Change %s could not be rebased due to a conflict during merge.\n\n"
+ + "merge conflict(s):\n"
+ + "fileName 2",
+ change2.getCommit().name(), change2.getChange().getId()));
assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterChange1);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index c57d285..016b1e6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -15,16 +15,24 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabelRemoval;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
+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.LabelId;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -39,56 +47,141 @@
import org.junit.Test;
public class DeleteVoteIT extends AbstractDaemonTest {
+ @Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Test
- public void deleteVoteOnChange() throws Exception {
- deleteVote(false);
+ public void deleteVoteOnChange_withRemoveLabelPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(false);
}
@Test
- public void deleteVoteOnRevision() throws Exception {
- deleteVote(true);
+ public void deleteVoteOnChange_withRemoveReviewerPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(false);
}
- private void deleteVote(boolean onRevisionLevel) throws Exception {
+ @Test
+ public void deleteVoteOnChange_noPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyCannotDeleteVote(false);
+ }
+
+ @Test
+ public void deleteVoteOnRevision_withRemoveLabelPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(true);
+ }
+
+ @Test
+ public void deleteVoteOnRevision_withRemoveReviewerPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(true);
+ }
+
+ @Test
+ public void deleteVoteOnRevision_noPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyCannotDeleteVote(true);
+ }
+
+ private void verifyDeleteVote(boolean onRevisionLevel) throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
PushOneCommit.Result r2 = amendChange(r.getChangeId());
- requestScopeOperations.setApiUser(user.id());
+ requestScopeOperations.setApiUser(admin.id());
+ recommend(r.getChangeId());
+
+ TestAccount user2 = accountCreator.user2();
+ requestScopeOperations.setApiUser(user2.id());
recommend(r.getChangeId());
sender.clear();
- String endPoint =
+ String deleteAdminVoteEndPoint =
"/changes/"
+ r.getChangeId()
+ (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ "/reviewers/"
- + user.id().toString()
+ + admin.id().toString()
+ "/votes/Code-Review";
- RestResponse response = adminRestSession.delete(endPoint);
+ RestResponse response = userRestSession.delete(deleteAdminVoteEndPoint);
response.assertNoContent();
List<FakeEmailSender.Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
FakeEmailSender.Message msg = messages.get(0);
- assertThat(msg.rcpt()).containsExactly(user.getNameEmail());
- assertThat(msg.body()).contains(admin.fullName() + " has removed a vote from this change.");
+ assertThat(msg.rcpt()).containsExactly(admin.getNameEmail(), user2.getNameEmail());
+ assertThat(msg.body()).contains(user.fullName() + " has removed a vote from this change.");
assertThat(msg.body())
- .contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n");
+ .contains("Removed Code-Review+1 by " + admin.fullName() + " <" + admin.email() + ">\n");
- endPoint =
+ String viewVotesEndPoint =
"/changes/"
+ r.getChangeId()
+ (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ "/reviewers/"
- + user.id().toString()
+ + admin.id().toString()
+ "/votes";
- response = adminRestSession.get(endPoint);
+ response = userRestSession.get(viewVotesEndPoint);
response.assertOK();
Map<String, Short> m =
@@ -99,14 +192,38 @@
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
ChangeMessageInfo message = Iterables.getLast(c.messages);
- assertThat(message.author._accountId).isEqualTo(admin.id().get());
+ assertThat(message.author._accountId).isEqualTo(user.id().get());
assertThat(message.message)
.isEqualTo(
String.format(
"Removed Code-Review+1 by %s\n",
- AccountTemplateUtil.getAccountTemplate(user.id())));
+ AccountTemplateUtil.getAccountTemplate(admin.id())));
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
- .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
+ .containsExactlyElementsIn(ImmutableSet.of(user2.id(), admin.id()));
+ }
+
+ private void verifyCannotDeleteVote(boolean onRevisionLevel) throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+
+ PushOneCommit.Result r2 = amendChange(r.getChangeId());
+
+ requestScopeOperations.setApiUser(admin.id());
+ recommend(r.getChangeId());
+
+ sender.clear();
+ String deleteAdminVoteEndPoint =
+ "/changes/"
+ + r.getChangeId()
+ + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ + "/reviewers/"
+ + admin.id().toString()
+ + "/votes/Code-Review";
+
+ RestResponse response = userRestSession.delete(deleteAdminVoteEndPoint);
+ response.assertForbidden();
+
+ assertThat(sender.getMessages()).isEmpty();
}
private Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
index 0c221aa..7b42d93 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance.rest.project;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.entities.RefNames.REFS_HEADS;
@@ -21,6 +22,7 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.IdString;
import org.junit.Test;
public class CreateChangeIT extends AbstractDaemonTest {
@@ -43,7 +45,44 @@
ChangeInput input = new ChangeInput();
input.branch = "foo";
input.subject = "subject";
- RestResponse cr = adminRestSession.post("/projects/" + project.get() + "/create.change", input);
- cr.assertCreated();
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertCreated();
+ }
+
+ @Test
+ public void nonMatchingProjectIsRejected() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = "non-matching-project";
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertBadRequest();
+ assertThat(response.getEntityContent()).isEqualTo("project must match URL");
+ }
+
+ @Test
+ public void matchingProjectIsAccepted() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = project.get();
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertCreated();
+ }
+
+ @Test
+ public void matchingProjectWithTrailingSlashIsAccepted() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = project.get() + "/";
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post(
+ "/projects/" + IdString.fromDecoded(project.get() + "/").encoded() + "/create.change",
+ input);
+ response.assertCreated();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 2123ac2..15baa78 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -1262,7 +1262,7 @@
+ c
+ "/comment/"
+ ps1List.get(0).id
- + " \n"
+ + " :\n"
+ "PS1, Line 1: initial\n"
+ "what happened to this?\n"
+ "\n"
@@ -1274,7 +1274,7 @@
+ c
+ "/comment/"
+ ps1List.get(1).id
- + " \n"
+ + " :\n"
+ "PS1, Line 1: boring\n"
+ "Is it that bad?\n"
+ "\n"
@@ -1288,7 +1288,7 @@
+ c
+ "/comment/"
+ ps2List.get(0).id
- + " \n"
+ + " :\n"
+ "PS2, Line 1: initial content\n"
+ "comment 1 on base\n"
+ "\n"
@@ -1300,7 +1300,7 @@
+ c
+ "/comment/"
+ ps2List.get(1).id
- + " \n"
+ + " :\n"
+ "PS2, Line 2: \n"
+ "comment 2 on base\n"
+ "\n"
@@ -1312,7 +1312,7 @@
+ c
+ "/comment/"
+ ps2List.get(2).id
- + " \n"
+ + " :\n"
+ "PS2, Line 1: interesting\n"
+ "better now\n"
+ "\n"
@@ -1324,7 +1324,7 @@
+ c
+ "/comment/"
+ ps2List.get(3).id
- + " \n"
+ + " :\n"
+ "PS2, Line 2: cntent\n"
+ "typo: content\n"
+ "\n"
diff --git a/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java b/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
index b2a0ded..e011ffc 100644
--- a/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
@@ -68,10 +68,7 @@
values = {"UiFeature__patchset_comments", "UiFeature__submit_requirements_ui"})
public void configOverride_defaultFeatureDisabled() {
assertThat(experimentFeatures.isFeatureEnabled("enabledFeature")).isTrue();
- assertThat(
- experimentFeatures.isFeatureEnabled(
- ExperimentFeaturesConstants.UI_FEATURE_PATCHSET_COMMENTS))
- .isFalse();
+ assertThat(experimentFeatures.isFeatureEnabled("UiFeature__patchset_comments")).isFalse();
assertThat(experimentFeatures.getEnabledExperimentFeatures()).containsExactly("enabledFeature");
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 1900158..cf1eee0 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -22,6 +22,7 @@
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.AccountGroup;
@@ -348,6 +349,163 @@
}
@Test
+ public void noNotificationForWatchKeywordWhenKeywordMatchesChangeOwner() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // watch keyword in project as user
+ watch(watchedProject, admin.email());
+
+ // push a change with owner=keyword -> should not trigger email notification
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+
+ // assert email notification for user
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void noNotificationForWatchKeywordWhenKeywordMatchesChangeReviewer() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // watch keyword in project as user
+ watch(watchedProject, user2.email());
+
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+ sender.clear();
+
+ // Add reviewer=keyword -> should trigger email notification only to new reviewer
+ gApi.changes().id(r.getChangeId()).addReviewer(user2.email());
+
+ // assert email notification
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertNotifyTo(user2);
+ assertThat(m.body()).contains("Change subject: subject\n");
+ }
+
+ @Test
+ public void watchOwner() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // watch keyword in project as user
+ watch(watchedProject, "owner:admin");
+
+ // push a change with keyword -> should trigger email notification
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+
+ // assert email notification for user
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(user.getNameEmail());
+ assertThat(m.body()).contains("Change subject: subject\n");
+ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+ sender.clear();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void watchNonVisibleOwner() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // watch keyword in project as user
+ watch(watchedProject, "owner:admin");
+
+ // Verify that 'user' can't see 'admin'
+ assertThatAccountIsNotVisible(admin);
+
+ // push a change with keyword -> should trigger email notification
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+
+ // assert no email notifications for user
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void watchChangesCommentedBySelf() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // user watches all changes that have a comment by themselves
+ watch(watchedProject, "commentby:self");
+
+ // pushing a change as admin should not trigger an email to user
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+ assertThat(sender.getMessages()).isEmpty();
+
+ // commenting by admin should not trigger an email to user
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.message = "A Comment";
+ gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ assertThat(sender.getMessages()).isEmpty();
+
+ // commenting by user matches the project watch, but doesn't send an email to user because
+ // CC_ON_OWN_COMMENTS is false by default, so the user is removed from the TO list, but an email
+ // is sent to the admin user
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(admin.getNameEmail());
+ assertThat(m.body()).contains("Change subject: subject\n");
+ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+ sender.clear();
+
+ // commenting by admin now triggers an email to user because the change has a comment by user
+ // and hence matches the project watch
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(user.getNameEmail());
+ assertThat(m.body()).contains("Change subject: subject\n");
+ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+ sender.clear();
+ }
+
+ @Test
public void watchAllProjects() throws Exception {
String anyProject = projectOperations.newProject().create().get();
requestScopeOperations.setApiUser(user.id());
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index c1a7627..661802e 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -18,11 +18,14 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabelRemoval;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelRemovalPermissionKey;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
@@ -443,6 +446,73 @@
}
@Test
+ public void addAllowLabelRemovalPermission() throws Exception {
+ Project.NameKey key = projectOperations.newProject().create();
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .add(allowLabelRemoval("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+ .update();
+
+ Config config = projectOperations.project(key).getConfig();
+ assertThat(config).sections().containsExactly("access", "submit");
+ assertThat(config).subsections("access").containsExactly("refs/foo");
+ assertThat(config)
+ .subsectionValues("access", "refs/foo")
+ .containsExactly("removeLabel-Code-Review", "-1..+2 group global:Registered-Users");
+ }
+
+ @Test
+ public void addBlockLabelRemovalPermission() throws Exception {
+ Project.NameKey key = projectOperations.newProject().create();
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .add(blockLabelRemoval("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+ .update();
+
+ Config config = projectOperations.project(key).getConfig();
+ assertThat(config).sections().containsExactly("access", "submit");
+ assertThat(config).subsections("access").containsExactly("refs/foo");
+ assertThat(config)
+ .subsectionValues("access", "refs/foo")
+ .containsExactly("removeLabel-Code-Review", "block -1..+2 group global:Registered-Users");
+ }
+
+ @Test
+ public void addAllowExclusiveLabelRemovalPermission() throws Exception {
+ Project.NameKey key = projectOperations.newProject().create();
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .add(allowLabelRemoval("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+ .setExclusiveGroup(labelRemovalPermissionKey("Code-Review").ref("refs/foo"), true)
+ .update();
+
+ Config config = projectOperations.project(key).getConfig();
+ assertThat(config).sections().containsExactly("access", "submit");
+ assertThat(config).subsections("access").containsExactly("refs/foo");
+ assertThat(config)
+ .subsectionValues("access", "refs/foo")
+ .containsExactly(
+ "removeLabel-Code-Review", "-1..+2 group global:Registered-Users",
+ "exclusiveGroupPermissions", "removeLabel-Code-Review");
+
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .setExclusiveGroup(labelRemovalPermissionKey("Code-Review").ref("refs/foo"), false)
+ .update();
+
+ config = projectOperations.project(key).getConfig();
+ assertThat(config).sections().containsExactly("access", "submit");
+ assertThat(config).subsections("access").containsExactly("refs/foo");
+ assertThat(config)
+ .subsectionValues("access", "refs/foo")
+ .containsExactly("removeLabel-Code-Review", "-1..+2 group global:Registered-Users");
+ }
+
+ @Test
public void addAllowCapability() throws Exception {
Config config = projectOperations.project(allProjects).getConfig();
assertThat(config)
@@ -540,6 +610,31 @@
}
@Test
+ public void removeLabelRemovalPermission() throws Exception {
+ Project.NameKey key = projectOperations.newProject().create();
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .add(allowLabelRemoval("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+ .add(allowLabelRemoval("Code-Review").ref("refs/foo").group(PROJECT_OWNERS).range(-2, 1))
+ .update();
+ assertThat(projectOperations.project(key).getConfig())
+ .subsectionValues("access", "refs/foo")
+ .containsExactly(
+ "removeLabel-Code-Review", "-1..+2 group global:Registered-Users",
+ "removeLabel-Code-Review", "-2..+1 group global:Project-Owners");
+
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .remove(labelRemovalPermissionKey("Code-Review").ref("refs/foo").group(REGISTERED_USERS))
+ .update();
+ assertThat(projectOperations.project(key).getConfig())
+ .subsectionValues("access", "refs/foo")
+ .containsExactly("removeLabel-Code-Review", "-2..+1 group global:Project-Owners");
+ }
+
+ @Test
public void removeCapability() throws Exception {
projectOperations
.allProjectsForUpdate()
diff --git a/javatests/com/google/gerrit/entities/PermissionTest.java b/javatests/com/google/gerrit/entities/PermissionTest.java
index 3175671..d25d833 100644
--- a/javatests/com/google/gerrit/entities/PermissionTest.java
+++ b/javatests/com/google/gerrit/entities/PermissionTest.java
@@ -36,6 +36,7 @@
assertThat(Permission.isPermission(Permission.LABEL + LabelId.CODE_REVIEW)).isTrue();
assertThat(Permission.isPermission(Permission.LABEL_AS + LabelId.CODE_REVIEW)).isTrue();
+ assertThat(Permission.isPermission(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW)).isTrue();
assertThat(Permission.isPermission(LabelId.CODE_REVIEW)).isFalse();
}
@@ -56,6 +57,7 @@
assertThat(Permission.isLabel(Permission.LABEL + LabelId.CODE_REVIEW)).isTrue();
assertThat(Permission.isLabel(Permission.LABEL_AS + LabelId.CODE_REVIEW)).isFalse();
+ assertThat(Permission.isLabel(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW)).isFalse();
assertThat(Permission.isLabel(LabelId.CODE_REVIEW)).isFalse();
}
@@ -66,10 +68,22 @@
assertThat(Permission.isLabelAs(Permission.LABEL + LabelId.CODE_REVIEW)).isFalse();
assertThat(Permission.isLabelAs(Permission.LABEL_AS + LabelId.CODE_REVIEW)).isTrue();
+ assertThat(Permission.isLabelAs(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW)).isFalse();
assertThat(Permission.isLabelAs(LabelId.CODE_REVIEW)).isFalse();
}
@Test
+ public void isRemoveLabel() {
+ assertThat(Permission.isRemoveLabel(Permission.ABANDON)).isFalse();
+ assertThat(Permission.isRemoveLabel("no-permission")).isFalse();
+
+ assertThat(Permission.isRemoveLabel(Permission.LABEL + LabelId.CODE_REVIEW)).isFalse();
+ assertThat(Permission.isRemoveLabel(Permission.LABEL_AS + LabelId.CODE_REVIEW)).isFalse();
+ assertThat(Permission.isRemoveLabel(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW)).isTrue();
+ assertThat(Permission.isRemoveLabel(LabelId.CODE_REVIEW)).isFalse();
+ }
+
+ @Test
public void forLabel() {
assertThat(Permission.forLabel(LabelId.CODE_REVIEW))
.isEqualTo(Permission.LABEL + LabelId.CODE_REVIEW);
@@ -82,11 +96,19 @@
}
@Test
+ public void forRemoveLabel() {
+ assertThat(Permission.forRemoveLabel(LabelId.CODE_REVIEW))
+ .isEqualTo(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW);
+ }
+
+ @Test
public void extractLabel() {
assertThat(Permission.extractLabel(Permission.LABEL + LabelId.CODE_REVIEW))
.isEqualTo(LabelId.CODE_REVIEW);
assertThat(Permission.extractLabel(Permission.LABEL_AS + LabelId.CODE_REVIEW))
.isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(Permission.extractLabel(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW))
+ .isEqualTo(LabelId.CODE_REVIEW);
assertThat(Permission.extractLabel(LabelId.CODE_REVIEW)).isNull();
assertThat(Permission.extractLabel(Permission.ABANDON)).isNull();
}
@@ -103,6 +125,10 @@
Permission.canBeOnAllProjects(
AccessSection.ALL, Permission.LABEL_AS + LabelId.CODE_REVIEW))
.isTrue();
+ assertThat(
+ Permission.canBeOnAllProjects(
+ AccessSection.ALL, Permission.REMOVE_LABEL + LabelId.CODE_REVIEW))
+ .isTrue();
assertThat(Permission.canBeOnAllProjects("refs/heads/*", Permission.ABANDON)).isTrue();
assertThat(Permission.canBeOnAllProjects("refs/heads/*", Permission.OWNER)).isTrue();
@@ -113,6 +139,10 @@
Permission.canBeOnAllProjects(
"refs/heads/*", Permission.LABEL_AS + LabelId.CODE_REVIEW))
.isTrue();
+ assertThat(
+ Permission.canBeOnAllProjects(
+ "refs/heads/*", Permission.REMOVE_LABEL + LabelId.CODE_REVIEW))
+ .isTrue();
}
@Test
@@ -126,6 +156,8 @@
.isEqualTo(LabelId.CODE_REVIEW);
assertThat(Permission.create(Permission.LABEL_AS + LabelId.CODE_REVIEW).getLabel())
.isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(Permission.create(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW).getLabel())
+ .isEqualTo(LabelId.CODE_REVIEW);
assertThat(Permission.create(LabelId.CODE_REVIEW).getLabel()).isNull();
assertThat(Permission.create(Permission.ABANDON).getLabel()).isNull();
}
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 7ed236a..f45d33b 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -48,6 +48,7 @@
assertThat(diff.added().messages).isNull();
assertThat(diff.added().reviewers).isNull();
assertThat(diff.added().hashtags).isNull();
+ assertThat(diff.added().removableLabels).isNull();
assertThat(diff.removed()._number).isNull();
assertThat(diff.removed().branch).isNull();
assertThat(diff.removed().project).isNull();
@@ -56,6 +57,7 @@
assertThat(diff.removed().messages).isNull();
assertThat(diff.removed().reviewers).isNull();
assertThat(diff.removed().hashtags).isNull();
+ assertThat(diff.removed().removableLabels).isNull();
}
@Test
@@ -315,6 +317,295 @@
}
@Test
+ public void getDiff_removableLabelsEmpty_returnsNullRemovableLabels() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ oldChangeInfo.removableLabels = ImmutableMap.of();
+ newChangeInfo.removableLabels = ImmutableMap.of();
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isNull();
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsNullAndEmpty_returnsEmptyRemovableLabels() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ newChangeInfo.removableLabels = ImmutableMap.of();
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isEmpty();
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsEmptyAndNull_returnsEmptyRemovableLabels() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ oldChangeInfo.removableLabels = ImmutableMap.of();
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isNull();
+ assertThat(diff.removed().removableLabels).isEmpty();
+ }
+
+ @Test
+ public void getDiff_removableLabelsLabelAdded() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "Cow";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "Pig";
+ AccountInfo acc3 = new AccountInfo();
+ acc3.name = "Cat";
+ AccountInfo acc4 = new AccountInfo();
+ acc4.name = "Dog";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)),
+ "Verified",
+ ImmutableMap.of("-1", ImmutableList.of(acc4)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Verified", ImmutableMap.of("-1", ImmutableList.of(acc4))));
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsLabelRemoved() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "Cow";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "Pig";
+ AccountInfo acc3 = new AccountInfo();
+ acc3.name = "Cat";
+ AccountInfo acc4 = new AccountInfo();
+ acc4.name = "Dog";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)),
+ "Verified",
+ ImmutableMap.of("-1", ImmutableList.of(acc4)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isNull();
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Verified", ImmutableMap.of("-1", ImmutableList.of(acc4))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsVoteAdded() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+ AccountInfo acc3 = new AccountInfo();
+ acc3.name = "acc3";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("-1", ImmutableList.of(acc2, acc3))));
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsVoteRemoved() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+ AccountInfo acc3 = new AccountInfo();
+ acc3.name = "acc3";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isNull();
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("-1", ImmutableList.of(acc2, acc3))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsAccountAdded() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1, acc2)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc2))));
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsAccountRemoved() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1, acc2)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc2))));
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsAccountChanged() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc2)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc2))));
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsScoreChanged() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("-1", ImmutableList.of(acc1)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("-1", ImmutableList.of(acc1))));
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsLabelChanged() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Verified", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Verified", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsLabelScoreAndAccountChanged() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Verified", ImmutableMap.of("-1", ImmutableList.of(acc2)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Verified", ImmutableMap.of("-1", ImmutableList.of(acc2))));
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ }
+
+ @Test
public void getDiff_assertCanConstructAllChangeInfoReferences() throws Exception {
buildObjectWithFullFields(ChangeInfo.class);
}
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
index f65e823..06ea8b6 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -60,15 +60,13 @@
String testCdnPath = "bar-cdn";
String testFaviconURL = "zaz-url";
- // Pick any known experiment enabled by default;
- String disabledDefault = ExperimentFeaturesConstants.UI_FEATURE_PATCHSET_COMMENTS;
- assertThat(ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES).contains(disabledDefault);
+ assertThat(ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES).isEmpty();
org.eclipse.jgit.lib.Config serverConfig = new org.eclipse.jgit.lib.Config();
serverConfig.setStringList(
"experiments", null, "enabled", ImmutableList.of("NewFeature", "DisabledFeature"));
serverConfig.setStringList(
- "experiments", null, "disabled", ImmutableList.of("DisabledFeature", disabledDefault));
+ "experiments", null, "disabled", ImmutableList.of("DisabledFeature"));
ExperimentFeatures experimentFeatures = new ConfigExperimentFeatures(serverConfig);
IndexServlet servlet =
new IndexServlet(
@@ -97,7 +95,6 @@
+ "\\x5b\\x5d\\x7d');");
ImmutableSet<String> enabledDefaults =
ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES.stream()
- .filter(e -> !e.equals(disabledDefault))
.collect(ImmutableSet.toImmutableSet());
List<String> expectedEnabled = new ArrayList<>();
expectedEnabled.add("NewFeature");
diff --git a/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java b/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
index 7f98a9d..affaadf 100644
--- a/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
+++ b/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
@@ -37,17 +37,17 @@
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.CHANGE_ID_SPEC)),
schema(
2,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)));
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.CHANGE_ID_SPEC)));
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.CHANGE_ID_SPEC)),
schema(
2,
ImmutableList.<FieldDef<ChangeData, ?>>of(),
@@ -58,8 +58,8 @@
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.CHANGE_ID_SPEC)),
schema(
2,
ImmutableList.<IndexedField<ChangeData, ?>>of(
@@ -81,8 +81,9 @@
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(
+ ChangeField.CHANGE_ID_SPEC)),
schema(
2,
ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.OWNER_FIELD),
@@ -105,9 +106,13 @@
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
- schema(2, ImmutableList.of(ID_MODIFIED), ImmutableList.of())));
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(
+ ChangeField.CHANGE_ID_SPEC)),
+ schema(
+ 2,
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ID_MODIFIED),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of())));
assertThat(e).hasMessageThat().contains("Fields may not be modified");
assertThat(e).hasMessageThat().contains(ChangeField.CHANGE_ID_FIELD.name());
}
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index 3658834..37728f7 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -23,6 +23,8 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResolver.Result;
import com.google.gerrit.server.account.AccountResolver.Searcher;
import com.google.gerrit.server.account.AccountResolver.StringSearcher;
@@ -35,6 +37,8 @@
import org.junit.Test;
public class AccountResolverTest {
+ private final CurrentUser user = new AnonymousUser();
+
private static class TestSearcher extends StringSearcher {
private final String pattern;
private final boolean shortCircuit;
@@ -282,7 +286,7 @@
AccountResolver resolver = newAccountResolver();
assertThat(
new UnresolvableAccountException(
- resolver.new Result("foo", ImmutableList.of(), ImmutableList.of())))
+ resolver.new Result("foo", ImmutableList.of(), ImmutableList.of(), user)))
.hasMessageThat()
.isEqualTo("Account 'foo' not found");
}
@@ -292,7 +296,7 @@
AccountResolver resolver = newAccountResolver();
UnresolvableAccountException e =
new UnresolvableAccountException(
- resolver.new Result("self", ImmutableList.of(), ImmutableList.of()));
+ resolver.new Result("self", ImmutableList.of(), ImmutableList.of(), user));
assertThat(e.isSelf()).isTrue();
assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login");
}
@@ -302,7 +306,7 @@
AccountResolver resolver = newAccountResolver();
UnresolvableAccountException e =
new UnresolvableAccountException(
- resolver.new Result("me", ImmutableList.of(), ImmutableList.of()));
+ resolver.new Result("me", ImmutableList.of(), ImmutableList.of(), user));
assertThat(e.isSelf()).isTrue();
assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login");
}
@@ -314,7 +318,10 @@
new UnresolvableAccountException(
resolver
.new Result(
- "foo", ImmutableList.of(newAccount(3), newAccount(1)), ImmutableList.of())))
+ "foo",
+ ImmutableList.of(newAccount(3), newAccount(1)),
+ ImmutableList.of(),
+ user)))
.hasMessageThat()
.isEqualTo(
"Account 'foo' is ambiguous (at most 3 shown):\n1: Anonymous Name (1)\n3: Anonymous Name (3)");
@@ -329,7 +336,8 @@
.new Result(
"foo",
ImmutableList.of(),
- ImmutableList.of(newInactiveAccount(3), newInactiveAccount(1)))))
+ ImmutableList.of(newInactiveAccount(3), newInactiveAccount(1)),
+ user)))
.hasMessageThat()
.isEqualTo(
"Account 'foo' only matches inactive accounts. To use an inactive account, retry"
@@ -352,10 +360,11 @@
Supplier<Predicate<AccountState>> visibilitySupplier,
Predicate<AccountState> activityPredicate)
throws Exception {
- return newAccountResolver().searchImpl(input, searchers, visibilitySupplier, activityPredicate);
+ return newAccountResolver()
+ .searchImpl(input, searchers, user, visibilitySupplier, activityPredicate);
}
- private static AccountResolver newAccountResolver() {
+ private AccountResolver newAccountResolver() {
return new AccountResolver(null, null, null, null, null, null, null, null, "Anonymous Name");
}
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index 65eb3b8..a40afe8 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -40,8 +40,7 @@
String metaId = "0e39795bb25dc914118224995c53c5c36923a461";
account.setMetaId(metaId);
Iterable<byte[]> refStates =
- (Iterable<byte[]>)
- AccountField.REF_STATE_SPEC.get(AccountState.forAccount(account.build()));
+ AccountField.REF_STATE_SPEC.get(AccountState.forAccount(account.build()));
List<String> values = toStrings(refStates);
String expectedValue =
allUsersName.get() + ":" + RefNames.refsUsers(account.id()) + ":" + metaId;
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index 95e8aa5..15adcf8 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -98,6 +98,11 @@
}
@Override
+ public void deleteByValue(ChangeData value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public void delete(Change.Id id) {
throw new UnsupportedOperationException();
}
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 37d8468..be8f1f9 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -76,6 +76,7 @@
import com.google.inject.TypeLiteral;
import java.time.Instant;
import java.time.ZoneId;
+import java.util.Optional;
import java.util.concurrent.ExecutorService;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -276,7 +277,7 @@
protected ChangeUpdate newUpdate(
Injector injector, Change c, CurrentUser user, boolean shouldExist) throws Exception {
- ChangeUpdate update = TestChanges.newUpdate(injector, c, user, shouldExist);
+ ChangeUpdate update = TestChanges.newUpdate(injector, c, Optional.of(user), shouldExist);
update.setPatchSetId(c.currentPatchSetId());
update.setAllowWriteToNewRef(true);
return update;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 61b5e55..9cd002e 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -825,7 +825,8 @@
ImmutableList.of(", ", ":\"", ",", "!@#$%^\0&*):\" \n: \r\"#$@,. :");
for (String strangeTag : strangeTags) {
Change c = newChange();
- CurrentUser otherUserAsOwner = userFactory.runAs(null, changeOwner.getAccountId(), otherUser);
+ CurrentUser otherUserAsOwner =
+ userFactory.runAs(/* remotePeer= */ null, changeOwner.getAccountId(), otherUser);
ChangeUpdate update = newUpdate(c, otherUserAsOwner);
update.putApproval(LabelId.CODE_REVIEW, (short) 2);
update.setTag(strangeTag);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index b53de89..25f2f98 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -354,7 +354,8 @@
@Test
public void realUser() throws Exception {
Change c = newChange();
- CurrentUser ownerAsOtherUser = userFactory.runAs(null, otherUserId, changeOwner);
+ CurrentUser ownerAsOtherUser =
+ userFactory.runAs(/* remotePeer= */ null, otherUserId, changeOwner);
ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
update.setChangeMessage("Message on behalf of other user");
update.commit();
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 5e6803e..527e78e 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -399,7 +399,9 @@
IdentifiedUser impersonatedChangeOwner =
this.userFactory.runAs(
- null, changeOwner.getAccountId(), requireNonNull(otherUser).getRealUser());
+ /* remotePeer= */ null,
+ changeOwner.getAccountId(),
+ requireNonNull(otherUser).getRealUser());
ChangeUpdate impersonatedChangeMessageUpdate = newUpdate(c, impersonatedChangeOwner);
impersonatedChangeMessageUpdate.setChangeMessage("Other comment on behalf of");
impersonatedChangeMessageUpdate.commit();
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 23f33fc..fbf9c87 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -46,6 +46,7 @@
import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.google.common.truth.ThrowableSubject;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.FakeSubmitRule;
@@ -109,12 +110,14 @@
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.ListGroupMembership;
import com.google.gerrit.server.account.VersionedAccountQueries;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeTriplet;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.group.testing.TestGroupBackend;
import com.google.gerrit.server.index.change.ChangeField;
@@ -122,6 +125,7 @@
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.index.change.IndexedChangeQuery;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
@@ -133,8 +137,7 @@
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.GerritServerTests;
-import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testing.TestChanges;
import com.google.gerrit.testing.TestTimeUtil;
import com.google.inject.Inject;
import com.google.inject.Injector;
@@ -158,7 +161,7 @@
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.SystemReader;
@@ -183,7 +186,7 @@
@Inject protected ChangeIndexer indexer;
@Inject protected ExtensionRegistry extensionRegistry;
@Inject protected IndexConfig indexConfig;
- @Inject protected InMemoryRepositoryManager repoManager;
+ @Inject protected GitRepositoryManager repoManager;
@Inject protected Provider<AnonymousUser> anonymousUserProvider;
@Inject protected Provider<InternalChangeQuery> queryProvider;
@Inject protected ChangeNotes.Factory notesFactory;
@@ -206,11 +209,20 @@
protected Injector injector;
protected LifecycleManager lifecycle;
+
+ /**
+ * Index tests should not use username in query assert, since some backends do not use {@link
+ * ExternalId#SCHEME_USERNAME}
+ */
protected Account.Id userId;
+
protected CurrentUser user;
+ protected Account userAccount;
private String systemTimeZone;
+ protected TestRepository<Repository> repo;
+
protected abstract Injector createInjector();
@Before
@@ -226,6 +238,10 @@
@After
public void cleanUp() {
+ if (repo != null) {
+ repo.close();
+ repo = null;
+ }
lifecycle.stop();
}
@@ -252,8 +268,9 @@
return () -> requestUser;
}
- protected void resetUser() {
+ protected void resetUser() throws ConfigInvalidException, IOException {
user = userFactory.create(userId);
+ userAccount = accounts.get(userId).get().account();
requestContext.setContext(newRequestContext(userId));
}
@@ -289,9 +306,9 @@
@Test
public void byId() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
assertQuery("12345");
assertQuery(change1.getId().get(), change1);
@@ -300,8 +317,8 @@
@Test
public void byKey() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
String key = change.getKey().get();
assertQuery("I0000000000000000000000000000000000000000");
@@ -313,8 +330,8 @@
@Test
public void byTriplet() throws Exception {
- TestRepository<Repo> repo = createProject("iabcde");
- Change change = insert(repo, newChangeForBranch(repo, "branch"));
+ repo = createAndOpenProject("iabcde");
+ Change change = insert("iabcde", newChangeForBranch(repo, "branch"));
String k = change.getKey().get();
assertQuery("iabcde~branch~" + k, change);
@@ -336,11 +353,11 @@
@Test
public void byStatus() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change2 = insert(repo, ins2);
+ Change change2 = insert("repo", ins2);
assertQuery("status:new", change1);
assertQuery("status:NEW", change1);
@@ -355,11 +372,11 @@
@Test
public void byStatusOr() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change2 = insert(repo, ins2);
+ Change change2 = insert("repo", ins2);
assertQuery("status:new OR status:merged", change2, change1);
assertQuery("status:new or status:merged", change2, change1);
@@ -367,10 +384,10 @@
@Test
public void byStatusOpen() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
- insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change1 = insert("repo", ins1);
+ insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
Change[] expected = new Change[] {change1};
assertQuery("status:open", expected);
@@ -389,12 +406,12 @@
@Test
public void byStatusClosed() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED);
- Change change2 = insert(repo, ins2);
- insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
+ Change change2 = insert("repo", ins2);
+ insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
Change[] expected = new Change[] {change2, change1};
assertQuery("status:closed", expected);
@@ -410,12 +427,12 @@
@Test
public void byStatusAbandoned() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED);
- insert(repo, ins1);
+ insert("repo", ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED);
- Change change1 = insert(repo, ins2);
- insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
+ Change change1 = insert("repo", ins2);
+ insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
assertQuery("status:abandoned", change1);
assertQuery("status:ABANDONED", change1);
@@ -424,10 +441,10 @@
@Test
public void byStatusPrefix() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
- insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change1 = insert("repo", ins1);
+ Change change2 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
assertQuery("status:n", change1);
assertQuery("status:ne", change1);
@@ -435,6 +452,7 @@
assertQuery("status:N", change1);
assertQuery("status:nE", change1);
assertQuery("status:neW", change1);
+ assertQuery("status:m", change2);
Exception thrown = assertThrows(BadRequestException.class, () -> assertQuery("status:newx"));
assertThat(thrown).hasMessageThat().isEqualTo("Unrecognized value: newx");
thrown = assertThrows(BadRequestException.class, () -> assertQuery("status:nx"));
@@ -443,11 +461,11 @@
@Test
public void byPrivate() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert(repo, newChange(repo), user2);
+ Change change2 = insert("repo", newChange(repo), user2);
// No private changes.
assertQuery("is:open", change2, change1);
@@ -467,8 +485,8 @@
@Test
public void byWip() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
assertQuery("is:open", change1);
assertQuery("is:wip");
@@ -485,8 +503,8 @@
@Test
public void excludeWipChangeFromReviewersDashboards() throws Exception {
Account.Id user1 = createAccount("user1");
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWorkInProgress(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWorkInProgress(repo), userId);
assertQuery("is:wip", change1);
assertQuery("reviewer:" + user1);
@@ -502,8 +520,8 @@
@Test
public void byStarted() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWorkInProgress(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWorkInProgress(repo));
assertQuery("is:started");
@@ -538,11 +556,11 @@
@Test
public void restorePendingReviewers() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- TestRepository<Repo> repo = createProject(project.get());
+ repo = createAndOpenProject(project.get());
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
- Change change1 = insert(repo, newChangeWorkInProgress(repo));
+ Change change1 = insert("repo", newChangeWorkInProgress(repo));
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
String email1 = "email1@example.com";
@@ -595,9 +613,9 @@
@Test
public void byCommit() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
- Change change = insert(repo, ins);
+ Change change = insert("repo", ins);
String sha = ins.getCommitId().name();
assertQuery("0000000000000000000000000000000000000000");
@@ -611,11 +629,11 @@
@Test
public void byOwner() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert(repo, newChange(repo), user2);
+ Change change2 = insert("repo", newChange(repo), user2);
assertQuery("is:owner", change1);
assertQuery("owner:" + userId.get(), change1);
@@ -628,15 +646,16 @@
@Test
public void byUploader() throws Exception {
assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue();
- Account.Id user2 =
- accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- CurrentUser user2CurrentUser = userFactory.create(user2);
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
assertQuery("is:uploader", change1);
assertQuery("uploader:" + userId.get(), change1);
- change1 = newPatchSet(repo, change1, user2CurrentUser, /* message= */ Optional.empty());
+
+ Account.Id user2 = createAccount("anotheruser");
+ CurrentUser user2CurrentUser = userFactory.create(user2);
+
+ change1 = newPatchSet("repo", change1, user2CurrentUser, /* message= */ Optional.empty());
// Uploader has changed
assertQuery("uploader:" + userId.get());
assertQuery("uploader:" + user2.get(), change1);
@@ -669,7 +688,7 @@
}
private void byAuthorOrCommitterExact(String searchOperator) throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ createProject("repo");
PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
PersonIdent john = new PersonIdent("John", "john@example.com");
PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
@@ -677,10 +696,10 @@
PersonIdent myself = new PersonIdent("I Am", ua.preferredEmail());
PersonIdent selfName = new PersonIdent("My Self", "my.self@example.com");
- Change change1 = createChange(repo, johnDoe);
- Change change2 = createChange(repo, john);
- Change change3 = createChange(repo, doeSmith);
- createChange(repo, selfName);
+ Change change1 = createChange("repo", johnDoe);
+ Change change2 = createChange("repo", john);
+ Change change3 = createChange("repo", doeSmith);
+ createChange("repo", selfName);
// Only email address.
assertQuery(searchOperator + "john.doe@example.com", change1);
@@ -705,19 +724,19 @@
assertQuery(searchOperator + "self");
// ':self' matches a change created with the current user's email address
- Change change5 = createChange(repo, myself);
+ Change change5 = createChange("repo", myself);
assertQuery(searchOperator + "me", change5);
assertQuery(searchOperator + "self", change5);
}
private void byAuthorOrCommitterFullText(String searchOperator) throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ createProject("repo");
PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
PersonIdent john = new PersonIdent("John", "john@example.com");
PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
- Change change1 = createChange(repo, johnDoe);
- Change change2 = createChange(repo, john);
- Change change3 = createChange(repo, doeSmith);
+ Change change1 = createChange("repo", johnDoe);
+ Change change2 = createChange("repo", john);
+ Change change3 = createChange("repo", doeSmith);
// By exact name.
assertQuery(searchOperator + "\"John Doe\"", change1);
@@ -738,20 +757,25 @@
assertThat(thrown).hasMessageThat().contains("invalid value");
}
- protected Change createChange(TestRepository<Repo> repo, PersonIdent person) throws Exception {
- RevCommit commit =
- repo.parseBody(repo.commit().message("message").author(person).committer(person).create());
- return insert(repo, newChangeForCommit(repo, commit), null);
+ @CanIgnoreReturnValue
+ protected Change createChange(String repoName, PersonIdent person) throws Exception {
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) {
+ RevCommit commit =
+ repo.parseBody(
+ repo.commit().message("message").author(person).committer(person).create());
+ return insert("repo", newChangeForCommit(repo, commit), null);
+ }
}
@Test
public void byOwnerIn() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert(repo, newChange(repo), user2);
- Change change3 = insert(repo, newChange(repo), user2);
+ Change change2 = insert("repo", newChange(repo), user2);
+ Change change3 = insert("repo", newChange(repo), user2);
gApi.changes().id(change3.getId().get()).current().review(ReviewInput.approve());
gApi.changes().id(change3.getId().get()).current().submit();
@@ -764,24 +788,25 @@
@Test
public void byUploaderIn() throws Exception {
assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
+
assertQuery("uploaderin:Administrators", change1);
- Account.Id user2 =
- accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
+ Account.Id user2 = createAccount("anotheruser");
CurrentUser user2CurrentUser = userFactory.create(user2);
- change1 = newPatchSet(repo, change1, user2CurrentUser, /* message= */ Optional.empty());
+ change1 = newPatchSet("repo", change1, user2CurrentUser, /* message= */ Optional.empty());
+
assertQuery("uploaderin:Administrators");
assertQuery("uploaderin:\"Registered Users\"", change1);
}
@Test
public void byProject() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("project:foo");
assertQuery("project:repo");
@@ -791,16 +816,16 @@
@Test
public void byProjectWithHidden() throws Exception {
- TestRepository<Repo> hiddenProject = createProject("hiddenProject");
- insert(hiddenProject, newChange(hiddenProject));
+ createProject("hiddenProject");
+ insert("hiddenProject", newChange("hiddenProject"));
projectOperations
.project(Project.nameKey("hiddenProject"))
.forUpdate()
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
- TestRepository<Repo> visibleProject = createProject("visibleProject");
- Change visibleChange = insert(visibleProject, newChange(visibleProject));
+ createProject("visibleProject");
+ Change visibleChange = insert("visibleProject", newChange("visibleProject"));
assertQuery("project:visibleProject", visibleChange);
assertQuery("project:hiddenProject");
assertQuery("project:visibleProject OR project:hiddenProject", visibleChange);
@@ -808,13 +833,13 @@
@Test
public void byParentOf() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- RevCommit commit1 = repo1.parseBody(repo1.commit().message("message").create());
- Change change1 = insert(repo1, newChangeForCommit(repo1, commit1));
- RevCommit commit2 = repo1.parseBody(repo1.commit(commit1));
- Change change2 = insert(repo1, newChangeForCommit(repo1, commit2));
- RevCommit commit3 = repo1.parseBody(repo1.commit(commit1, commit2));
- Change change3 = insert(repo1, newChangeForCommit(repo1, commit3));
+ repo = createAndOpenProject("repo1");
+ RevCommit commit1 = repo.parseBody(repo.commit().message("message").create());
+ Change change1 = insert("repo1", newChangeForCommit(repo, commit1));
+ RevCommit commit2 = repo.parseBody(repo.commit(commit1));
+ Change change2 = insert("repo1", newChangeForCommit(repo, commit2));
+ RevCommit commit3 = repo.parseBody(repo.commit(commit1, commit2));
+ Change change3 = insert("repo1", newChangeForCommit(repo, commit3));
assertQuery("parentof:" + change1.getId().get());
assertQuery("parentof:" + change1.getKey().get());
@@ -826,10 +851,10 @@
@Test
public void byParentProject() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2", "repo1");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2", "repo1");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("parentproject:repo1", change2, change1);
assertQuery("parentproject:repo2", change2);
@@ -837,10 +862,10 @@
@Test
public void byProjectPrefix() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("projects:foo");
assertQuery("projects:repo1", change1);
@@ -850,10 +875,10 @@
@Test
public void byRepository() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("repository:foo");
assertQuery("repository:repo");
@@ -863,10 +888,10 @@
@Test
public void byParentRepository() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2", "repo1");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2", "repo1");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("parentrepository:repo1", change2, change1);
assertQuery("parentrepository:repo2", change2);
@@ -874,10 +899,10 @@
@Test
public void byRepositoryPrefix() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("repositories:foo");
assertQuery("repositories:repo1", change1);
@@ -887,10 +912,10 @@
@Test
public void byRepo() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("repo:foo");
assertQuery("repo:repo");
@@ -900,10 +925,10 @@
@Test
public void byParentRepo() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2", "repo1");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2", "repo1");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("parentrepo:repo1", change2, change1);
assertQuery("parentrepo:repo2", change2);
@@ -911,10 +936,10 @@
@Test
public void byRepoPrefix() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("repos:foo");
assertQuery("repos:repo1", change1);
@@ -924,9 +949,9 @@
@Test
public void byBranchAndRef() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeForBranch(repo, "master"));
- Change change2 = insert(repo, newChangeForBranch(repo, "branch"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeForBranch(repo, "master"));
+ Change change2 = insert("repo", newChangeForBranch(repo, "branch"));
assertQuery("branch:foo");
assertQuery("branch:master", change1);
@@ -943,26 +968,26 @@
@Test
public void byTopic() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithTopic(repo, "feature2");
- Change change2 = insert(repo, ins2);
+ Change change2 = insert("repo", ins2);
ChangeInserter ins3 = newChangeWithTopic(repo, "Cherrypick-feature2");
- Change change3 = insert(repo, ins3);
+ Change change3 = insert("repo", ins3);
ChangeInserter ins4 = newChangeWithTopic(repo, "feature2-fixup");
- Change change4 = insert(repo, ins4);
+ Change change4 = insert("repo", ins4);
ChangeInserter ins5 = newChangeWithTopic(repo, "https://gerrit.local");
- Change change5 = insert(repo, ins5);
+ Change change5 = insert("repo", ins5);
ChangeInserter ins6 = newChangeWithTopic(repo, "git_gerrit_training");
- Change change6 = insert(repo, ins6);
+ Change change6 = insert("repo", ins6);
- Change change_no_topic = insert(repo, newChange(repo));
+ Change changeNoTopic = insert("repo", newChange(repo));
assertQuery("intopic:foo");
assertQuery("intopic:feature1", change1);
@@ -971,8 +996,8 @@
assertQuery("intopic:feature2", change4, change3, change2);
assertQuery("intopic:fixup", change4);
assertQuery("intopic:gerrit", change6, change5);
- assertQuery("topic:\"\"", change_no_topic);
- assertQuery("intopic:\"\"", change_no_topic);
+ assertQuery("topic:\"\"", changeNoTopic);
+ assertQuery("intopic:\"\"", changeNoTopic);
assume().that(getSchema().hasField(ChangeField.PREFIX_TOPIC)).isTrue();
assertQuery("prefixtopic:feature", change4, change2, change1);
@@ -982,16 +1007,16 @@
@Test
public void byTopicRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithTopic(repo, "Cherrypick-feature1");
- Change change2 = insert(repo, ins2);
+ Change change2 = insert("repo", ins2);
ChangeInserter ins3 = newChangeWithTopic(repo, "feature1-fixup");
- Change change3 = insert(repo, ins3);
+ Change change3 = insert("repo", ins3);
assertQuery("intopic:^feature1.*", change3, change1);
assertQuery("intopic:{^.*feature1$}", change2, change1);
@@ -999,13 +1024,13 @@
@Test
public void byMessageExact() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("A great \"fix\" to my bug").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
assertQuery("message:foo");
assertQuery("message:one", change1);
@@ -1016,16 +1041,16 @@
@Test
public void byMessageRegEx() throws Exception {
assume().that(getSchema().hasField(ChangeField.COMMIT_MESSAGE_EXACT)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("aaaabcc").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("aaaacc").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("Title\n\nHELLO WORLD").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
RevCommit commit4 =
repo.parseBody(repo.commit().message("Title\n\nfoobar hello WORLD").create());
- Change change4 = insert(repo, newChangeForCommit(repo, commit4));
+ Change change4 = insert("repo", newChangeForCommit(repo, commit4));
assertQuery("message:\"^aaaa(b|c)*\"", change2, change1);
assertQuery("message:\"^aaaa(c)*c.*\"", change2);
@@ -1037,7 +1062,7 @@
@Test
public void bySubject() throws Exception {
assume().that(getSchema().hasField(ChangeField.SUBJECT_SPEC)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 =
repo.parseBody(
repo.commit()
@@ -1046,7 +1071,7 @@
+ "Message body\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b920")
.create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 =
repo.parseBody(
repo.commit()
@@ -1055,7 +1080,7 @@
+ "Message body for another commit\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
.create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 =
repo.parseBody(
repo.commit()
@@ -1064,7 +1089,7 @@
+ "Last message body\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
.create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
assertQuery("subject:First", change1);
assertQuery("subject:Second", change2);
@@ -1074,7 +1099,7 @@
assertQuery("subject:body");
change1 =
newPatchSet(
- repo,
+ "repo",
change1,
user,
Optional.of("Rework of commit with test subject\n\n" + "Message body\n\n"));
@@ -1084,12 +1109,60 @@
}
@Test
+ public void bySubjectPrefix() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.PREFIX_SUBJECT_SPEC)).isTrue();
+ repo = createAndOpenProject("repo");
+ RevCommit commit1 =
+ repo.parseBody(
+ repo.commit()
+ .message(
+ "[FOO123] First commit with test subject\n\n"
+ + "Message body\n\n"
+ + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b920")
+ .create());
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ RevCommit commit2 =
+ repo.parseBody(
+ repo.commit()
+ .message(
+ "[BAR45] Second commit with test subject\n\n"
+ + "Message body for another commit\n\n"
+ + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
+ .create());
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ RevCommit commit3 =
+ repo.parseBody(
+ repo.commit()
+ .message(
+ "[FOO99] Third commit with test subject\n\n"
+ + "Last message body\n\n"
+ + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
+ .create());
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+
+ assertQuery("prefixsubject:\"[FOO\"", change3, change1);
+ assertQuery("prefixsubject:\"[BAR\"", change2);
+ assertQuery("prefixsubject:\"[FOO1\"", change1);
+ assertQuery("prefixsubject:\"[FOO123]\"", change1);
+ assertQuery("prefixsubject:\"[\"", change3, change2, change1);
+ assertQuery("prefixsubject:FOO");
+ change1 =
+ newPatchSet(
+ "repo",
+ change1,
+ user,
+ Optional.of("[BAR123] Rework of commit with test subject\n\n" + "Message body\n\n"));
+ assertQuery("prefixsubject:\"[FOO\"", change3);
+ assertQuery("prefixsubject:\"[BAR\"", change1, change2);
+ }
+
+ @Test
public void fullTextWithNumbers() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("12345 67890").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("12346 67891").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
assertQuery("message:1234");
assertQuery("message:12345", change1);
@@ -1098,13 +1171,13 @@
@Test
public void fullTextMultipleTerms() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("Signed-off: owner").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Signed by owner").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("This change is off").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
assertQuery("message:\"Signed-off: owner\"", change1);
assertQuery("message:\"Signed\"", change2, change1);
@@ -1113,11 +1186,11 @@
@Test
public void byMessageMixedCase() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("Hello gerrit").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Hello Gerrit").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
assertQuery("message:gerrit", change2, change1);
assertQuery("message:Gerrit", change2, change1);
@@ -1125,16 +1198,16 @@
@Test
public void byMessageSubstring() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("https://gerrit.local").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
assertQuery("message:gerrit", change1);
}
@Test
public void byLabel() throws Exception {
- accountManager.authenticate(authRequestFactory.createForUser("anotheruser"));
- TestRepository<Repo> repo = createProject("repo");
+ Account.Id anotherUser = createAccount("anotheruser");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
ChangeInserter ins2 = newChange(repo);
ChangeInserter ins3 = newChange(repo);
@@ -1142,24 +1215,24 @@
ChangeInserter ins5 = newChange(repo);
ChangeInserter ins6 = newChange(repo);
- Change reviewMinus2Change = insert(repo, ins);
+ Change reviewMinus2Change = insert("repo", ins);
gApi.changes().id(reviewMinus2Change.getId().get()).current().review(ReviewInput.reject());
- Change reviewMinus1Change = insert(repo, ins2);
+ Change reviewMinus1Change = insert("repo", ins2);
gApi.changes().id(reviewMinus1Change.getId().get()).current().review(ReviewInput.dislike());
- Change noLabelChange = insert(repo, ins3);
+ Change noLabelChange = insert("repo", ins3);
- Change reviewPlus1Change = insert(repo, ins4);
+ Change reviewPlus1Change = insert("repo", ins4);
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
- Change reviewTwoPlus1Change = insert(repo, ins5);
+ Change reviewTwoPlus1Change = insert("repo", ins5);
gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
requestContext.setContext(newRequestContext(createAccount("user1")));
gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
requestContext.setContext(newRequestContext(userId));
- Change reviewPlus2Change = insert(repo, ins6);
+ Change reviewPlus2Change = insert("repo", ins6);
gApi.changes().id(reviewPlus2Change.getId().get()).current().review(ReviewInput.approve());
Map<String, Short> m =
@@ -1223,9 +1296,15 @@
assertQuery("label:Code-Review<=-2", reviewMinus2Change);
assertQuery("label:Code-Review<-2");
- assertQuery("label:Code-Review=+1,anotheruser");
- assertQuery("label:Code-Review=+1,user", reviewTwoPlus1Change, reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1," + anotherUser);
+ assertQuery(
+ String.format("label:Code-Review=+1,%s", userAccount.preferredEmail()),
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery(
+ String.format("label:Code-Review=+1,user=%s", userAccount.preferredEmail()),
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
assertQuery("label:Code-Review=+1,Administrators", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery(
"label:Code-Review=+1,group=Administrators", reviewTwoPlus1Change, reviewPlus1Change);
@@ -1292,9 +1371,8 @@
@Test
public void byLabelMulti() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Project.NameKey project =
- Project.nameKey(repo.getRepository().getDescription().getRepositoryName());
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project.get());
LabelType verified =
label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
@@ -1321,25 +1399,25 @@
ChangeInserter ins5 = newChange(repo);
// CR+1
- Change reviewCRplus1 = insert(repo, ins);
+ Change reviewCRplus1 = insert(project.get(), ins);
gApi.changes().id(reviewCRplus1.getId().get()).current().review(ReviewInput.recommend());
// CR+2
- Change reviewCRplus2 = insert(repo, ins2);
+ Change reviewCRplus2 = insert(project.get(), ins2);
gApi.changes().id(reviewCRplus2.getId().get()).current().review(ReviewInput.approve());
// CR+1 VR+1
- Change reviewCRplus1VRplus1 = insert(repo, ins3);
+ Change reviewCRplus1VRplus1 = insert(project.get(), ins3);
gApi.changes().id(reviewCRplus1VRplus1.getId().get()).current().review(ReviewInput.recommend());
gApi.changes().id(reviewCRplus1VRplus1.getId().get()).current().review(reviewVerified);
// CR+2 VR+1
- Change reviewCRplus2VRplus1 = insert(repo, ins4);
+ Change reviewCRplus2VRplus1 = insert(project.get(), ins4);
gApi.changes().id(reviewCRplus2VRplus1.getId().get()).current().review(ReviewInput.approve());
gApi.changes().id(reviewCRplus2VRplus1.getId().get()).current().review(reviewVerified);
// VR+1
- Change reviewVRplus1 = insert(repo, ins5);
+ Change reviewVRplus1 = insert(project.get(), ins5);
gApi.changes().id(reviewVRplus1.getId().get()).current().review(reviewVerified);
assertQuery("label:Code-Review=+1", reviewCRplus1VRplus1, reviewCRplus1);
@@ -1358,28 +1436,28 @@
@Test
public void byLabelNotOwner() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
Account.Id user1 = createAccount("user1");
- Change reviewPlus1Change = insert(repo, ins);
+ Change reviewPlus1Change = insert("repo", ins);
// post a review with user1
requestContext.setContext(newRequestContext(user1));
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
- assertQuery("label:Code-Review=+1,user=user1", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=" + user1, reviewPlus1Change);
assertQuery("label:Code-Review=+1,owner");
}
@Test
public void byLabelNonUploader() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
Account.Id user1 = createAccount("user1");
// create a change with "user"
- Change reviewPlus1Change = insert(repo, ins);
+ Change reviewPlus1Change = insert("repo", ins);
// add a +1 vote with "user". Query doesn't match since voter is the uploader.
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
@@ -1418,8 +1496,8 @@
@Test
public void byLabelGroup() throws Exception {
Account.Id user1 = createAccount("user1");
- createAccount("user2");
- TestRepository<Repo> repo = createProject("repo");
+ Account.Id user2 = createAccount("user2");
+ repo = createAndOpenProject("repo");
// create group and add users
String g1 = createGroup("group1", "Administrators");
@@ -1428,7 +1506,7 @@
gApi.groups().id(g2).addMembers("user2");
// create a change
- Change change1 = insert(repo, newChange(repo), user1);
+ Change change1 = insert("repo", newChange(repo), user1);
// post a review with user1
requestContext.setContext(newRequestContext(user1));
@@ -1441,8 +1519,8 @@
requestContext.setContext(newRequestContext(userId));
assertQuery("label:Code-Review=+1,group1", change1);
assertQuery("label:Code-Review=+1,group=group1", change1);
- assertQuery("label:Code-Review=+1,user=user1", change1);
- assertQuery("label:Code-Review=+1,user=user2");
+ assertQuery("label:Code-Review=+1,user=" + user1, change1);
+ assertQuery("label:Code-Review=+1,user=" + user2);
assertQuery("label:Code-Review=+1,group=group2");
}
@@ -1450,7 +1528,7 @@
public void byLabelExternalGroup() throws Exception {
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
- TestRepository<InMemoryRepositoryManager.Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
// create group and add users
AccountGroup.UUID external_group1 = AccountGroup.uuid("testbackend:group1");
@@ -1462,8 +1540,8 @@
testGroupBackend.setMembershipsOf(
user2, new ListGroupMembership(ImmutableList.of(external_group2)));
- Change change1 = insert(repo, newChange(repo), user1);
- Change change2 = insert(repo, newChange(repo), user1);
+ Change change1 = insert("repo", newChange(repo), user1);
+ Change change2 = insert("repo", newChange(repo), user1);
// post a review with user1 and other_user
requestContext.setContext(newRequestContext(user1));
@@ -1479,18 +1557,18 @@
assertQuery("label:Code-Review=+1," + external_group1.get(), change1);
assertQuery("label:Code-Review=+1,group=" + external_group1.get(), change1);
- assertQuery("label:Code-Review=+1,user=user1", change1);
- assertQuery("label:Code-Review=+1,user=user2");
+ assertQuery("label:Code-Review=+1,user=" + user1, change1);
+ assertQuery("label:Code-Review=+1,user=" + user2);
assertQuery("label:Code-Review=+1,group=" + external_group2.get());
}
@Test
public void limit() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Change last = null;
int n = 5;
for (int i = 0; i < n; i++) {
- last = insert(repo, newChange(repo));
+ last = insert("repo", newChange(repo));
}
for (int i = 1; i <= n + 2; i++) {
@@ -1515,10 +1593,10 @@
@Test
public void start() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 2; i++) {
- changes.add(insert(repo, newChange(repo)));
+ changes.add(insert("repo", newChange(repo)));
}
assertQuery("status:new", changes.get(1), changes.get(0));
@@ -1535,10 +1613,10 @@
@Test
public void startWithLimit() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 3; i++) {
- changes.add(insert(repo, newChange(repo)));
+ changes.add(insert("repo", newChange(repo)));
}
assertQuery("status:new limit:2", changes.get(2), changes.get(1));
@@ -1549,8 +1627,8 @@
@Test
public void maxPages() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
QueryRequest query = newQuery("status:new").withLimit(10);
assertQuery(query, change);
@@ -1565,12 +1643,12 @@
@Test
public void updateOrder() throws Exception {
resetTimeWithClockStep(2, MINUTES);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
List<ChangeInserter> inserters = new ArrayList<>();
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 5; i++) {
inserters.add(newChange(repo));
- changes.add(insert(repo, inserters.get(i)));
+ changes.add(insert("repo", inserters.get(i)));
}
for (int i : ImmutableList.of(2, 0, 1, 4, 3)) {
@@ -1592,10 +1670,10 @@
@Test
public void updatedOrder() throws Exception {
resetTimeWithClockStep(1, SECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChange(repo);
- Change change1 = insert(repo, ins1);
- Change change2 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", ins1);
+ Change change2 = insert("repo", newChange(repo));
assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2));
assertQuery("status:new", change2, change1);
@@ -1613,12 +1691,12 @@
@Test
public void filterOutMoreThanOnePageOfResults() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
for (int i = 0; i < 5; i++) {
- insert(repo, newChange(repo), user2);
+ insert("repo", newChange(repo), user2);
}
assertQuery("status:new ownerin:Administrators", change);
@@ -1627,11 +1705,11 @@
@Test
public void filterOutAllResults() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
for (int i = 0; i < 5; i++) {
- insert(repo, newChange(repo), user2);
+ insert("repo", newChange(repo), user2);
}
assertQuery("status:new ownerin:Administrators");
@@ -1640,8 +1718,8 @@
@Test
public void byFileExact() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("file:file");
assertQuery("file:dir", change);
@@ -1653,8 +1731,8 @@
@Test
public void byFileRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("file:.*file.*");
assertQuery("file:^file.*"); // Whole path only.
@@ -1663,8 +1741,8 @@
@Test
public void byPathExact() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("path:file");
assertQuery("path:dir");
@@ -1676,8 +1754,8 @@
@Test
public void byPathRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("path:.*file.*");
assertQuery("path:^dir.file.*", change);
@@ -1685,12 +1763,12 @@
@Test
public void byExtension() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc"));
- Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC"));
- Change change3 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
- Change change4 = insert(repo, newChangeWithFiles(repo, "Quux.java", "foo"));
- Change change5 = insert(repo, newChangeWithFiles(repo, "foo"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithFiles(repo, "foo.h", "foo.cc"));
+ Change change2 = insert("repo", newChangeWithFiles(repo, "bar.H", "bar.CC"));
+ Change change3 = insert("repo", newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+ Change change4 = insert("repo", newChangeWithFiles(repo, "Quux.java", "foo"));
+ Change change5 = insert("repo", newChangeWithFiles(repo, "foo"));
assertQuery("extension:java", change4);
assertQuery("ext:java", change4);
@@ -1706,14 +1784,14 @@
@Test
public void byOnlyExtensions() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
- Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
- Change change3 = insert(repo, newChangeWithFiles(repo, "foo.CC", "bar.cc"));
- Change change4 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
- Change change5 = insert(repo, newChangeWithFiles(repo, "Quux.java"));
- Change change6 = insert(repo, newChangeWithFiles(repo, "foo.txt", "foo"));
- Change change7 = insert(repo, newChangeWithFiles(repo, "foo"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
+ Change change2 = insert("repo", newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
+ Change change3 = insert("repo", newChangeWithFiles(repo, "foo.CC", "bar.cc"));
+ Change change4 = insert("repo", newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+ Change change5 = insert("repo", newChangeWithFiles(repo, "Quux.java"));
+ Change change6 = insert("repo", newChangeWithFiles(repo, "foo.txt", "foo"));
+ Change change7 = insert("repo", newChangeWithFiles(repo, "foo"));
// case doesn't matter
assertQuery("onlyextensions:cc,h", change4, change2, change1);
@@ -1753,23 +1831,23 @@
@Test
public void byFooter() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nfoo: baz").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar\nfoo:baz").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar=baz").create());
- Change change4 = insert(repo, newChangeForCommit(repo, commit4));
+ Change change4 = insert("repo", newChangeForCommit(repo, commit4));
// create a changes with lines that look like footers, but which are not
RevCommit commit5 =
repo.parseBody(
repo.commit().message("Test\n\nfoo: bar\n\nfoo=bar").insertChangeId().create());
- Change change5 = insert(repo, newChangeForCommit(repo, commit5));
+ Change change5 = insert("repo", newChangeForCommit(repo, commit5));
RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
- insert(repo, newChangeForCommit(repo, commit6));
+ insert("repo", newChangeForCommit(repo, commit6));
// matching by 'key=value' works
assertQuery("footer:foo=bar", change3, change1);
@@ -1803,15 +1881,15 @@
@Test
public void byFooterName() throws Exception {
assume().that(getSchema().hasField(ChangeField.FOOTER_NAME)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nBaR: baz").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
// create a changes with lines that look like footers, but which are not
RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
- insert(repo, newChangeForCommit(repo, commit6));
+ insert("repo", newChangeForCommit(repo, commit6));
// matching by 'key=value' works
assertQuery("hasfooter:foo", change1);
@@ -1823,14 +1901,14 @@
@Test
public void byDirectory() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
- Change change2 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
+ Change change2 = insert("repo", newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
Change change3 =
- insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
- Change change4 = insert(repo, newChangeWithFiles(repo, "a.txt"));
- Change change5 = insert(repo, newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
- Change change6 = insert(repo, newChangeWithFiles(repo, "all/caps/DIRECTORY/file.txt"));
+ insert("repo", newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+ Change change4 = insert("repo", newChangeWithFiles(repo, "a.txt"));
+ Change change5 = insert("repo", newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
+ Change change6 = insert("repo", newChangeWithFiles(repo, "all/caps/DIRECTORY/file.txt"));
// matching by directory prefix works
assertQuery("directory:src", change2, change1);
@@ -1891,10 +1969,10 @@
@Test
public void byDirectoryRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
Change change2 =
- insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+ insert("repo", newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
// match by regexp
assertQuery("directory:^.*va.*", change1);
@@ -1904,9 +1982,9 @@
@Test
public void byComment() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
- Change change = insert(repo, ins);
+ Change change = insert("repo", ins);
ReviewInput input = new ReviewInput();
input.message = "toplevel";
@@ -1934,11 +2012,11 @@
public void byAge() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
// Stop time so age queries use the same endpoint.
TestTimeUtil.setClockStep(0, MILLISECONDS);
@@ -1975,11 +2053,11 @@
public void byBeforeUntil() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
// Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -2027,11 +2105,11 @@
public void byAfterSince() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
// Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -2071,12 +2149,12 @@
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
submit(change3);
@@ -2131,12 +2209,12 @@
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
assertThat(TimeUtil.nowMs()).isEqualTo(startMs);
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
@@ -2201,13 +2279,13 @@
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
submit(change2);
@@ -2234,15 +2312,15 @@
@Test
public void bySize() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
// added = 3, deleted = 0, delta = 3
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "foo\n\foo\nfoo").create());
// added = 0, deleted = 2, delta = 2
RevCommit commit2 = repo.parseBody(repo.commit().parent(commit1).add("file1", "foo").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
assertQuery("added:>4");
assertQuery("-added:<=4");
@@ -2290,9 +2368,9 @@
}
private List<Change> setUpHashtagChanges() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
addHashtags(change1.getId(), "foo", "aaa-bbb-ccc");
addHashtags(change2.getId(), "foo", "bar", "a tag", "ACamelCaseTag");
@@ -2339,10 +2417,10 @@
@Test
public void byHashtagRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
addHashtags(change1.getId(), "feature1");
addHashtags(change1.getId(), "trending");
addHashtags(change2.getId(), "Cherrypick-feature1");
@@ -2355,27 +2433,27 @@
@Test
public void byDefault() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
- Change change1 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
RevCommit commit2 = repo.parseBody(repo.commit().message("foosubject").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().add("Foo.java", "foo contents").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
ChangeInserter ins4 = newChange(repo);
- Change change4 = insert(repo, ins4);
+ Change change4 = insert("repo", ins4);
ReviewInput ri4 = new ReviewInput();
ri4.message = "toplevel";
ri4.labels = ImmutableMap.of("Code-Review", (short) 1);
gApi.changes().id(change4.getId().get()).current().review(ri4);
ChangeInserter ins5 = newChangeWithTopic(repo, "feature5");
- Change change5 = insert(repo, ins5);
+ Change change5 = insert("repo", ins5);
- Change change6 = insert(repo, newChangeForBranch(repo, "branch6"));
+ Change change6 = insert("repo", newChangeForBranch(repo, "branch6"));
assertQuery(change1.getId().get(), change1);
assertQuery(ChangeTriplet.format(change1), change1);
@@ -2396,18 +2474,18 @@
@Test
public void byDefaultWithCommitPrefix() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit = repo.parseBody(repo.commit().message("message").create());
- Change change = insert(repo, newChangeForCommit(repo, commit));
+ Change change = insert("repo", newChangeForCommit(repo, commit));
assertQuery(commit.getId().getName().substring(0, 6), change);
}
@Test
public void visible() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChangePrivate(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChangePrivate(repo));
String q = "project:repo";
@@ -2461,8 +2539,8 @@
// Switch to user3
requestContext.setContext(newRequestContext(user3));
- Change change3 = insert(repo, newChange(repo), user3);
- Change change4 = insert(repo, newChangePrivate(repo), user3);
+ Change change3 = insert("repo", newChange(repo), user3);
+ Change change4 = insert("repo", newChangePrivate(repo), user3);
// User3 can see both their changes and the first user's change
assertQuery(q + " visibleto:" + user3.get(), change4, change3, change1);
@@ -2502,9 +2580,9 @@
@Test
public void visibleToSelf() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
gApi.changes().id(change2.getChangeId()).setPrivate(true, "private");
@@ -2520,16 +2598,12 @@
@Test
public void byCommentBy() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- int user2 =
- accountManager
- .authenticate(authRequestFactory.createForUser("anotheruser"))
- .getAccountId()
- .get();
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Account.Id user2 = createAccount("anotheruser");
ReviewInput input = new ReviewInput();
input.message = "toplevel";
ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
@@ -2550,8 +2624,8 @@
public void bySubmitRuleResult() throws Exception {
try (Registration registration =
extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
// The fake submit rule exports its ruleName as "FakeSubmitRule"
assertQuery("rule:FakeSubmitRule");
@@ -2570,17 +2644,17 @@
public void byNonExistingSubmitRule_returnsEmpty() throws Exception {
try (Registration registration =
extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
- TestRepository<Repo> repo = createProject("repo");
- insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ insert("repo", newChange(repo));
assertQuery("rule:non-existent-rule");
}
}
@Test
public void byHasDraft() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
assertQuery("has:draft");
@@ -2612,8 +2686,8 @@
*/
public void byHasDraftExcludesZombieDrafts() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- TestRepository<Repo> repo = createProject(project.get());
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject(project.get());
+ Change change = insert("repo", newChange(repo));
Change.Id id = change.getId();
DraftInput in = new DraftInput();
@@ -2625,7 +2699,7 @@
assertQuery("has:draft", change);
assertQuery("commentby:" + userId);
- try (TestRepository<Repo> allUsers =
+ try (TestRepository<Repository> allUsers =
new TestRepository<>(repoManager.openRepository(allUsersName))) {
Ref draftsRef = allUsers.getRepository().exactRef(RefNames.refsDraftComments(id, userId));
assertThat(draftsRef).isNotNull();
@@ -2649,15 +2723,15 @@
@Test
public void byHasDraftWithManyDrafts() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Change[] changesWithDrafts = new Change[30];
// unrelated change not shown in the result.
- insert(repo, newChange(repo));
+ insert("repo", newChange(repo));
for (int i = 0; i < changesWithDrafts.length; i++) {
// put the changes in reverse order since this is the order we receive them from the index.
- changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(repo, newChange(repo));
+ changesWithDrafts[changesWithDrafts.length - 1 - i] = insert("repo", newChange(repo));
DraftInput in = new DraftInput();
in.line = 1;
in.message = "nit: trailing whitespace";
@@ -2677,10 +2751,10 @@
@Test
public void byStarredBy() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ insert("repo", newChange(repo));
gApi.accounts().self().starChange(change1.getId().toString());
gApi.accounts().self().starChange(change2.getId().toString());
@@ -2696,8 +2770,8 @@
@Test
public void byStar() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
@@ -2712,11 +2786,11 @@
@Test
public void byStarWithManyStars() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Change[] changesWithDrafts = new Change[30];
for (int i = 0; i < changesWithDrafts.length; i++) {
// put the changes in reverse order since this is the order we receive them from the index.
- changesWithDrafts[changesWithDrafts.length - 1 - i] = insert(repo, newChange(repo));
+ changesWithDrafts[changesWithDrafts.length - 1 - i] = insert("repo", newChange(repo));
// star the change
gApi.accounts()
@@ -2730,12 +2804,12 @@
@Test
public void byFrom() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert(repo, newChange(repo), user2);
+ Change change2 = insert("repo", newChange(repo), user2);
ReviewInput input = new ReviewInput();
input.message = "toplevel";
@@ -2751,7 +2825,7 @@
@Test
public void conflicts() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 =
repo.parseBody(
repo.commit()
@@ -2763,10 +2837,10 @@
RevCommit commit3 =
repo.parseBody(repo.commit().add("dir/file2", "contents2 different").create());
RevCommit commit4 = repo.parseBody(repo.commit().add("file4", "contents4").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
- Change change4 = insert(repo, newChangeForCommit(repo, commit4));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+ Change change4 = insert("repo", newChangeForCommit(repo, commit4));
assertQuery("conflicts:" + change1.getId().get(), change3);
assertQuery("conflicts:" + change2.getId().get());
@@ -2779,11 +2853,12 @@
name = "change.mergeabilityComputationBehavior",
value = "API_REF_UPDATED_AND_CHANGE_REINDEX")
public void mergeable() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ assume().that(getSchema().hasField(ChangeField.MERGEABLE_SPEC)).isTrue();
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
RevCommit commit2 = repo.parseBody(repo.commit().add("file1", "contents2").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
assertQuery("conflicts:" + change1.getId().get(), change2);
assertQuery("conflicts:" + change2.getId().get(), change1);
@@ -2807,9 +2882,9 @@
@Test
public void cherrypick() throws Exception {
assume().that(getSchema().hasField(ChangeField.CHERRY_PICK_SPEC)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newCherryPickChange(repo, "foo", change1.currentPatchSetId()));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newCherryPickChange(repo, "foo", change1.currentPatchSetId()));
assertQuery("is:cherrypick", change2);
assertQuery("-is:cherrypick", change1);
@@ -2818,14 +2893,14 @@
@Test
public void merge() throws Exception {
assume().that(getSchema().hasField(ChangeField.MERGE_SPEC)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
RevCommit commit2 = repo.parseBody(repo.commit().add("file1", "contents2").create());
RevCommit commit3 =
repo.parseBody(repo.commit().parent(commit2).add("file1", "contents3").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
RevCommit mergeCommit =
repo.branch("master")
.commit()
@@ -2834,7 +2909,7 @@
.parent(commit3)
.insertChangeId()
.create();
- Change mergeChange = insert(repo, newChangeForCommit(repo, mergeCommit));
+ Change mergeChange = insert("repo", newChangeForCommit(repo, mergeCommit));
assertQuery("status:open is:merge", mergeChange);
assertQuery("status:open -is:merge", change3, change2, change1);
@@ -2844,10 +2919,10 @@
@Test
public void reviewedBy() throws Exception {
resetTimeWithClockStep(2, MINUTES);
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
gApi.changes().id(change1.getId().get()).current().review(new ReviewInput().message("comment"));
@@ -2858,7 +2933,7 @@
gApi.changes().id(change2.getId().get()).current().review(new ReviewInput().message("comment"));
PatchSet.Id ps3_1 = change3.currentPatchSetId();
- change3 = newPatchSet(repo, change3, user, /* message= */ Optional.empty());
+ change3 = newPatchSet("repo", change3, user, /* message= */ Optional.empty());
assertThat(change3.currentPatchSetId()).isNotEqualTo(ps3_1);
// Response to previous patch set still counts as reviewing.
gApi.changes()
@@ -2885,11 +2960,11 @@
@Test
public void reviewerAndCc() throws Exception {
Account.Id user1 = createAccount("user1");
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
- insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
+ insert("repo", newChange(repo));
ReviewerInput rin = new ReviewerInput();
rin.reviewer = user1.toString();
@@ -2916,11 +2991,11 @@
@Test
public void byReviewed() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Account.Id otherUser =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
assertQuery("is:reviewed");
assertQuery("status:reviewed");
@@ -2944,11 +3019,11 @@
accountManager.authenticate(authRequestFactory.createForUser("user2")).getAccountId();
Account.Id user3 =
accountManager.authenticate(authRequestFactory.createForUser("user3")).getAccountId();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
ReviewerInput rin = new ReviewerInput();
rin.reviewer = user1.toString();
@@ -2988,7 +3063,7 @@
@Test
public void reviewerAndCcByEmail() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- TestRepository<Repo> repo = createProject(project.get());
+ repo = createAndOpenProject(project.get());
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
@@ -2996,9 +3071,9 @@
String userByEmail = "un.registered@reviewer.com";
String userByEmailWithName = "John Doe <" + userByEmail + ">";
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ insert("repo", newChange(repo));
ReviewerInput rin = new ReviewerInput();
rin.reviewer = userByEmailWithName;
@@ -3021,16 +3096,16 @@
@Test
public void reviewerAndCcByEmailWithQueryForDifferentUser() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- TestRepository<Repo> repo = createProject(project.get());
+ repo = createAndOpenProject(project.get());
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
String userByEmail = "John Doe <un.registered@reviewer.com>";
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ insert("repo", newChange(repo));
ReviewerInput rin = new ReviewerInput();
rin.reviewer = userByEmail;
@@ -3049,9 +3124,9 @@
@Test
public void submitRecords() throws Exception {
Account.Id user1 = createAccount("user1");
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
gApi.changes().id(change1.getId().get()).current().review(ReviewInput.approve());
requestContext.setContext(newRequestContext(user1));
@@ -3062,7 +3137,7 @@
assertQuery("-is:submittable", change2);
assertQuery("label:CodE-RevieW=ok", change1);
- assertQuery("label:CodE-RevieW=ok,user=user", change1);
+ assertQuery("label:CodE-RevieW=ok,user=" + userAccount.preferredEmail(), change1);
assertQuery("label:CodE-RevieW=ok,Administrators", change1);
assertQuery("label:CodE-RevieW=ok,group=Administrators", change1);
assertQuery("label:CodE-RevieW=ok,owner", change1);
@@ -3081,10 +3156,10 @@
public void hasEdit() throws Exception {
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
String changeId1 = change1.getKey().get();
- Change change2 = insert(repo, newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
String changeId2 = change2.getKey().get();
requestContext.setContext(newRequestContext(user1));
@@ -3105,10 +3180,10 @@
@Test
public void byUnresolved() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
// Change1 has one resolved comment (unresolvedcount = 0)
// Change2 has one unresolved comment (unresolvedcount = 1)
@@ -3136,13 +3211,13 @@
@Test
public void byCommitsOnBranchNotMerged() throws Exception {
- TestRepository<Repo> tr = createProject("repo");
- testByCommitsOnBranchNotMerged(tr, ImmutableSet.of());
+ createProject("repo");
+ testByCommitsOnBranchNotMerged("repo", ImmutableSet.of());
}
@Test
public void byCommitsOnBranchNotMergedSkipsMissingChanges() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ObjectId missing =
repo.branch(PatchSet.id(Change.id(987654), 1).toRefName())
.commit()
@@ -3150,72 +3225,75 @@
.insertChangeId()
.create()
.copy();
- testByCommitsOnBranchNotMerged(repo, ImmutableSet.of(missing));
+ testByCommitsOnBranchNotMerged("repo", ImmutableSet.of(missing));
}
- private void testByCommitsOnBranchNotMerged(TestRepository<Repo> repo, Collection<ObjectId> extra)
+ private void testByCommitsOnBranchNotMerged(String repo, Collection<ObjectId> extra)
throws Exception {
int n = 10;
List<String> shas = new ArrayList<>(n + extra.size());
extra.forEach(i -> shas.add(i.name()));
List<Integer> expectedIds = new ArrayList<>(n);
BranchNameKey dest = null;
- for (int i = 0; i < n; i++) {
- ChangeInserter ins = newChange(repo);
- insert(repo, ins);
- if (dest == null) {
- dest = ins.getChange().getDest();
+ try (TestRepository<Repository> repository =
+ new TestRepository<>(repoManager.openRepository(Project.nameKey(repo)))) {
+ for (int i = 0; i < n; i++) {
+ ChangeInserter ins = newChange(repository);
+ insert("repo", ins);
+ if (dest == null) {
+ dest = ins.getChange().getDest();
+ }
+ shas.add(ins.getCommitId().name());
+ expectedIds.add(ins.getChange().getId().get());
}
- shas.add(ins.getCommitId().name());
- expectedIds.add(ins.getChange().getId().get());
}
-
- for (int i = 1; i <= 11; i++) {
- Iterable<ChangeData> cds =
- queryProvider.get().byCommitsOnBranchNotMerged(repo.getRepository(), dest, shas, i);
- Iterable<Integer> ids = FluentIterable.from(cds).transform(in -> in.getId().get());
- String name = "limit " + i;
- assertWithMessage(name).that(ids).hasSize(n);
- assertWithMessage(name).that(ids).containsExactlyElementsIn(expectedIds);
+ try (Repository repository = repoManager.openRepository(Project.nameKey(repo))) {
+ for (int i = 1; i <= 11; i++) {
+ Iterable<ChangeData> cds =
+ queryProvider.get().byCommitsOnBranchNotMerged(repository, dest, shas, i);
+ Iterable<Integer> ids = FluentIterable.from(cds).transform(in -> in.getId().get());
+ String name = "limit " + i;
+ assertWithMessage(name).that(ids).hasSize(n);
+ assertWithMessage(name).that(ids).containsExactlyElementsIn(expectedIds);
+ }
}
}
@Test
public void reindexIfStale() throws Exception {
- Account.Id user = createAccount("user");
Project.NameKey project = Project.nameKey("repo");
- TestRepository<Repo> repo = createProject(project.get());
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject(project.get());
+ Change change = insert("repo", newChange(repo));
String changeId = change.getKey().get();
- ChangeNotes notes = notesFactory.create(change.getProject(), change.getId());
- PatchSet ps = psUtil.get(notes, change.currentPatchSetId());
- requestContext.setContext(newRequestContext(user));
- gApi.changes().id(changeId).edit().create();
- assertQuery("has:edit", change);
+ Account.Id anotherUser = createAccount("another-user");
+ requestContext.setContext(newRequestContext(anotherUser));
+ gApi.changes().id(changeId).addReviewer(anotherUser.toString());
+
+ assertQuery("reviewer:self", change);
assertThat(indexer.reindexIfStale(project, change.getId()).get()).isFalse();
- // Delete edit ref behind index's back.
- RefUpdate ru = repo.getRepository().updateRef(RefNames.refsEdit(user, change.getId(), ps.id()));
- ru.setForceUpdate(true);
- assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ // Remove reviewer behind index's back.
+ ChangeUpdate update = newUpdate(change);
+ update.removeReviewer(anotherUser);
+ update.commit();
// Index is stale.
- assertQuery("has:edit", change);
+ assertQuery("reviewer:self", change);
assertThat(indexer.reindexIfStale(project, change.getId()).get()).isTrue();
- assertQuery("has:edit");
+ assertQuery("reviewer:self");
}
@Test
public void watched() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
+ createProject("repo");
+ ChangeInserter ins1 = newChangeWithStatus("repo", Change.Status.NEW);
+ Change change1 = insert("repo", ins1);
- TestRepository<Repo> repo2 = createProject("repo2");
+ createProject("repo2");
- ChangeInserter ins2 = newChangeWithStatus(repo2, Change.Status.NEW);
- insert(repo2, ins2);
+ ChangeInserter ins2 = newChangeWithStatus("repo2", Change.Status.NEW);
+ insert("repo2", ins2);
assertQuery("is:watched");
@@ -3235,17 +3313,17 @@
@Test
public void trackingid() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 =
repo.parseBody(repo.commit().message("Change one\n\nBug:QUERY123").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 =
repo.parseBody(repo.commit().message("Change two\n\nIssue: Issue 16038\n").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 =
repo.parseBody(repo.commit().message("Change two\n\nGoogle-Bug-Id: b/16039\n").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
assertQuery("tr:QUERY123", change1);
assertQuery("bug:QUERY123", change1);
@@ -3271,9 +3349,9 @@
@Test
public void revertOf() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
// Create two commits and revert second commit (initial commit can't be reverted)
- Change initial = insert(repo, newChange(repo));
+ Change initial = insert("repo", newChange(repo));
gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(initial.getChangeId()).current().submit();
@@ -3288,10 +3366,10 @@
@Test
public void submissionId() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
// create irrelevant change
- insert(repo, newChange(repo));
+ insert("repo", newChange(repo));
gApi.changes().id(change.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(change.getChangeId()).current().submit();
String submissionId = gApi.changes().id(change.getChangeId()).get().submissionId;
@@ -3361,9 +3439,9 @@
return this;
}
- DashboardChangeState create(TestRepository<Repo> repo) throws Exception {
+ DashboardChangeState create(TestRepository<Repository> repo) throws Exception {
requestContext.setContext(newRequestContext(ownerId));
- Change change = insert(repo, newChange(repo), ownerId);
+ Change change = insert("repo", newChange(repo), ownerId);
id = change.getId();
ChangeApi cApi = gApi.changes().id(change.getChangeId());
if (assigneeId != null) {
@@ -3431,7 +3509,7 @@
@Test
public void dashboardHasUnpublishedDrafts() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Account.Id otherAccountId = createAccount("other");
DashboardChangeState hasUnpublishedDraft =
new DashboardChangeState(otherAccountId).draftCommentBy(user.getAccountId()).create(repo);
@@ -3449,7 +3527,7 @@
@Test
public void dashboardAssignedReviews() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Account.Id otherAccountId = createAccount("other");
DashboardChangeState otherOpenWip =
new DashboardChangeState(otherAccountId).wip().assignTo(user.getAccountId()).create(repo);
@@ -3471,12 +3549,12 @@
// Viewing another user's dashboard.
requestContext.setContext(newRequestContext(otherAccountId));
assertDashboardQuery(
- user.getUserName().get(), IndexPreloadingUtil.DASHBOARD_ASSIGNED_QUERY, otherOpenWip);
+ userId.toString(), IndexPreloadingUtil.DASHBOARD_ASSIGNED_QUERY, otherOpenWip);
}
@Test
public void dashboardWorkInProgressReviews() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
DashboardChangeState ownedOpenWip =
new DashboardChangeState(user.getAccountId()).wip().create(repo);
@@ -3491,7 +3569,7 @@
@Test
public void dashboardOutgoingReviews() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Account.Id otherAccountId = createAccount("other");
DashboardChangeState ownedOpenReviewable =
new DashboardChangeState(user.getAccountId()).create(repo);
@@ -3506,14 +3584,12 @@
// Viewing another user's dashboard.
requestContext.setContext(newRequestContext(otherAccountId));
assertDashboardQuery(
- user.getUserName().get(),
- IndexPreloadingUtil.DASHBOARD_OUTGOING_QUERY,
- ownedOpenReviewable);
+ userId.toString(), IndexPreloadingUtil.DASHBOARD_OUTGOING_QUERY, ownedOpenReviewable);
}
@Test
public void dashboardIncomingReviews() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Account.Id otherAccountId = createAccount("other");
DashboardChangeState reviewingReviewable =
new DashboardChangeState(otherAccountId).addReviewer(user.getAccountId()).create(repo);
@@ -3539,7 +3615,7 @@
// Viewing another user's dashboard.
requestContext.setContext(newRequestContext(otherAccountId));
assertDashboardQuery(
- user.getUserName().get(),
+ userId.toString(),
IndexPreloadingUtil.DASHBOARD_INCOMING_QUERY,
assignedReviewable,
reviewingReviewable);
@@ -3547,7 +3623,7 @@
@Test
public void dashboardRecentlyClosedReviews() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Account.Id otherAccountId = createAccount("other");
DashboardChangeState mergedOwned =
new DashboardChangeState(user.getAccountId()).mergeBy(user.getAccountId()).create(repo);
@@ -3610,7 +3686,7 @@
// Viewing another user's dashboard.
requestContext.setContext(newRequestContext(otherAccountId));
assertDashboardQuery(
- user.getUserName().get(),
+ userId.toString(),
IndexPreloadingUtil.DASHBOARD_RECENTLY_CLOSED_QUERY,
abandonedAssignedWip,
abandonedAssigned,
@@ -3626,9 +3702,9 @@
public void attentionSetIndexed() throws Exception {
assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS)).isTrue();
assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS_COUNT)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
AttentionSetInput input = new AttentionSetInput(userId.toString(), "some reason");
gApi.changes().id(change1.getChangeId()).addToAttentionSet(input);
@@ -3637,7 +3713,7 @@
assertQuery("-is:attention", change2);
assertQuery("has:attention", change1);
assertQuery("-has:attention", change2);
- assertQuery("attention:" + user.getUserName().get(), change1);
+ assertQuery("attention:" + userAccount.preferredEmail(), change1);
assertQuery("-attention:" + userId.toString(), change2);
gApi.changes()
@@ -3650,8 +3726,8 @@
@Test
public void attentionSetStored() throws Exception {
assume().that(getSchema().hasField(ChangeField.ATTENTION_SET_USERS)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
AttentionSetInput input = new AttentionSetInput(userId.toString(), "reason 1");
gApi.changes().id(change.getChangeId()).addToAttentionSet(input);
@@ -3679,29 +3755,29 @@
@Test
public void assignee() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
AssigneeInput input = new AssigneeInput();
- input.assignee = user.getUserName().get();
+ input.assignee = user.getAccountId().toString();
gApi.changes().id(change1.getChangeId()).setAssignee(input);
assertQuery("is:assigned", change1);
assertQuery("-is:assigned", change2);
assertQuery("is:unassigned", change2);
assertQuery("-is:unassigned", change1);
- assertQuery("assignee:" + user.getUserName().get(), change1);
- assertQuery("-assignee:" + user.getUserName().get(), change2);
+ assertQuery("assignee:" + user.getAccountId(), change1);
+ assertQuery("-assignee:" + user.getAccountId(), change2);
}
@GerritConfig(name = "accounts.visibility", value = "NONE")
@Test
public void userDestination() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- Change change1 = insert(repo1, newChange(repo1));
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ Change change1 = insert("repo1", newChange("repo1"));
+ createProject("repo2");
+ Change change2 = insert("repo2", newChange("repo2"));
assertThatQueryException("destination:foo")
.hasMessageThat()
@@ -3715,7 +3791,7 @@
String destination4 = "refs/heads/master\trepo3";
String destination5 = "refs/heads/other\trepo1";
- try (TestRepository<Repo> allUsers =
+ try (TestRepository<Repository> allUsers =
new TestRepository<>(repoManager.openRepository(allUsersName))) {
String refsUsers = RefNames.refsUsers(userId);
allUsers.branch(refsUsers).commit().add("destinations/destination1", destination1).create();
@@ -3766,26 +3842,25 @@
assertThatQueryException("destination:destination3,user=" + anotherUserId)
.hasMessageThat()
.isEqualTo("Unknown named destination: destination3");
- assertThatQueryException("destination:destination3,user=test")
+ assertThatQueryException("destination:destination3,user=non-existent")
.hasMessageThat()
- .isEqualTo("Account 'test' not found");
+ .isEqualTo("Account 'non-existent' not found");
requestContext.setContext(newRequestContext(anotherUserId));
- // account 1000000 is not visible to 'anotheruser' as they are not an admin
+ // account userId is not visible to 'anotheruser' as they are not an admin
assertThatQueryException("destination:destination3,user=" + userId)
.hasMessageThat()
- .isEqualTo("Account '1000000' not found");
+ .isEqualTo(String.format("Account '%s' not found", userId));
}
@GerritConfig(name = "accounts.visibility", value = "NONE")
@Test
public void userQuery() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChangeForBranch(repo, "stable"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChangeForBranch(repo, "stable"));
- Account.Id anotherUserId =
- accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
+ Account.Id anotherUserId = createAccount("anotheruser");
String queryListText =
"query1\tproject:repo\n"
+ "query2\tproject:repo status:open\n"
@@ -3797,7 +3872,7 @@
+ "query7\tproject:repo branch:stable\n"
+ "query8\tproject:repo branch:other";
- try (TestRepository<Repo> allUsers =
+ try (TestRepository<Repository> allUsers =
new TestRepository<>(repoManager.openRepository(allUsersName));
MetaDataUpdate md = metaDataUpdateFactory.create(allUsersName);
MetaDataUpdate anotherMd = metaDataUpdateFactory.create(allUsersName)) {
@@ -3811,19 +3886,20 @@
anotherQueries.commit(anotherMd);
}
+ assertThat(gApi.accounts().self().get()._accountId).isEqualTo(userId.get());
assertThatQueryException("query:foo").hasMessageThat().isEqualTo("Unknown named query: foo");
assertThatQueryException("query:query1,user=" + anotherUserId)
.hasMessageThat()
.isEqualTo("Unknown named query: query1");
- assertThatQueryException("query:query1,user=test")
+ assertThatQueryException("query:query1,user=non-existent")
.hasMessageThat()
- .isEqualTo("Account 'test' not found");
+ .isEqualTo("Account 'non-existent' not found");
requestContext.setContext(newRequestContext(anotherUserId));
// account 1000000 is not visible to 'anotheruser' as they are not an admin
assertThatQueryException("query:query1,user=" + userId)
.hasMessageThat()
- .isEqualTo("Account '1000000' not found");
+ .isEqualTo(String.format("Account '%s' not found", userId));
requestContext.setContext(newRequestContext(userId));
assertQuery("query:query1", change2, change1);
@@ -3842,16 +3918,16 @@
@Test
public void byOwnerInvalidQuery() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ insert("repo", newChange(repo), userId);
String nameEmail = user.asIdentifiedUser().getNameEmail();
assertQuery("owner: \"" + nameEmail + "\"\\");
}
@Test
public void byDeletedChange() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
String query = "change:" + change.getId();
assertQuery(query, change);
@@ -3862,8 +3938,8 @@
@Test
public void byUrlEncodedProject() throws Exception {
- TestRepository<Repo> repo = createProject("repo+foo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo+foo");
+ Change change = insert("repo+foo", newChange(repo));
assertQuery("project:repo+foo", change);
}
@@ -3896,9 +3972,9 @@
@Test
public void isPureRevert() throws Exception {
assume().that(getSchema().hasField(ChangeField.IS_PURE_REVERT_SPEC)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
// Create two commits and revert second commit (initial commit can't be reverted)
- Change initial = insert(repo, newChange(repo));
+ Change initial = insert("repo", newChange(repo));
gApi.changes().id(initial.getChangeId()).current().review(ReviewInput.approve());
gApi.changes().id(initial.getChangeId()).current().submit();
@@ -3942,8 +4018,8 @@
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
gApi.changes().id(change.getId().get()).addReviewer(user2.toString());
RequestContext adminContext = requestContext.setContext(newRequestContext(user2));
@@ -3958,8 +4034,8 @@
@Test
public void none() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
assertQuery(ChangeIndexPredicate.none());
@@ -3979,27 +4055,24 @@
@Test
@GerritConfig(name = "change.mergeabilityComputationBehavior", value = "NEVER")
public void mergeableFailsWhenNotIndexed() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ assume().that(getSchema().hasField(ChangeField.MERGE_SPEC)).isTrue();
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
- insert(repo, newChangeForCommit(repo, commit1));
+ insert("repo", newChangeForCommit(repo, commit1));
Throwable thrown = assertThrows(Throwable.class, () -> assertQuery("status:open is:mergeable"));
assertThat(thrown.getCause()).isInstanceOf(QueryParseException.class);
assertThat(thrown)
.hasMessageThat()
- .contains("'is:mergeable' operator is not supported by server");
+ .contains("'is:mergeable' operator is not supported on this gerrit host");
}
- protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
- return newChange(repo, null, null, null, null, null, false, false);
- }
-
- protected ChangeInserter newChangeForCommit(TestRepository<Repo> repo, RevCommit commit)
+ protected ChangeInserter newChangeForCommit(TestRepository<Repository> repo, RevCommit commit)
throws Exception {
return newChange(repo, commit, null, null, null, null, false, false);
}
- protected ChangeInserter newChangeWithFiles(TestRepository<Repo> repo, String... paths)
+ protected ChangeInserter newChangeWithFiles(TestRepository<Repository> repo, String... paths)
throws Exception {
TestRepository<?>.CommitBuilder b = repo.commit().message("Change with files");
for (String path : paths) {
@@ -4008,36 +4081,67 @@
return newChangeForCommit(repo, repo.parseBody(b.create()));
}
- protected ChangeInserter newChangeForBranch(TestRepository<Repo> repo, String branch)
+ protected ChangeInserter newChangeForBranch(TestRepository<Repository> repo, String branch)
throws Exception {
return newChange(repo, null, branch, null, null, null, false, false);
}
- protected ChangeInserter newChangeWithStatus(TestRepository<Repo> repo, Change.Status status)
- throws Exception {
+ protected ChangeInserter newChangeWithStatus(
+ TestRepository<Repository> repo, Change.Status status) throws Exception {
return newChange(repo, null, null, status, null, null, false, false);
}
- protected ChangeInserter newChangeWithTopic(TestRepository<Repo> repo, String topic)
+ protected ChangeInserter newChangeWithStatus(String repoName, Change.Status status)
+ throws Exception {
+ return newChange(repoName, null, null, status, null, null, false, false);
+ }
+
+ protected ChangeInserter newChangeWithTopic(TestRepository<Repository> repo, String topic)
throws Exception {
return newChange(repo, null, null, null, topic, null, false, false);
}
- protected ChangeInserter newChangeWorkInProgress(TestRepository<Repo> repo) throws Exception {
+ protected ChangeInserter newChangeWorkInProgress(TestRepository<Repository> repo)
+ throws Exception {
return newChange(repo, null, null, null, null, null, true, false);
}
- protected ChangeInserter newChangePrivate(TestRepository<Repo> repo) throws Exception {
+ protected ChangeInserter newChangePrivate(TestRepository<Repository> repo) throws Exception {
return newChange(repo, null, null, null, null, null, false, true);
}
protected ChangeInserter newCherryPickChange(
- TestRepository<Repo> repo, String branch, PatchSet.Id cherryPickOf) throws Exception {
+ TestRepository<Repository> repo, String branch, PatchSet.Id cherryPickOf) throws Exception {
return newChange(repo, null, branch, null, null, cherryPickOf, false, true);
}
+ protected ChangeInserter newChange(String repoName) throws Exception {
+ return newChange(repoName, null, null, null, null, null, false, false);
+ }
+
protected ChangeInserter newChange(
- TestRepository<Repo> repo,
+ String repoName,
+ @Nullable RevCommit commit,
+ @Nullable String branch,
+ @Nullable Change.Status status,
+ @Nullable String topic,
+ @Nullable PatchSet.Id cherryPickOf,
+ boolean workInProgress,
+ boolean isPrivate)
+ throws Exception {
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) {
+ return newChange(
+ repo, commit, branch, status, topic, cherryPickOf, workInProgress, isPrivate);
+ }
+ }
+
+ protected ChangeInserter newChange(TestRepository<Repository> repo) throws Exception {
+ return newChange(repo, null, null, null, null, null, false, false);
+ }
+
+ protected ChangeInserter newChange(
+ TestRepository<Repository> repo,
@Nullable RevCommit commit,
@Nullable String branch,
@Nullable Change.Status status,
@@ -4047,7 +4151,7 @@
boolean isPrivate)
throws Exception {
if (commit == null) {
- commit = repo.parseBody(repo.commit().message("message").create());
+ commit = repo.parseBody(repo.commit().message("initial message").create());
}
branch = MoreObjects.firstNonNull(branch, "refs/heads/master");
@@ -4056,32 +4160,32 @@
}
Change.Id id = Change.id(seq.nextChangeId());
- ChangeInserter ins =
- changeFactory
- .create(id, commit, branch)
- .setValidate(false)
- .setStatus(status)
- .setTopic(topic)
- .setWorkInProgress(workInProgress)
- .setPrivate(isPrivate)
- .setCherryPickOf(cherryPickOf);
- return ins;
+ return changeFactory
+ .create(id, commit, branch)
+ .setValidate(false)
+ .setStatus(status)
+ .setTopic(topic)
+ .setWorkInProgress(workInProgress)
+ .setPrivate(isPrivate)
+ .setCherryPickOf(cherryPickOf);
}
- protected Change insert(TestRepository<Repo> repo, ChangeInserter ins) throws Exception {
- return insert(repo, ins, null, TimeUtil.now());
- }
-
- protected Change insert(TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner)
+ @CanIgnoreReturnValue
+ protected Change insert(String repoName, ChangeInserter ins, @Nullable Account.Id owner)
throws Exception {
- return insert(repo, ins, owner, TimeUtil.now());
+ return insert(repoName, ins, owner, TimeUtil.now());
}
+ @CanIgnoreReturnValue
+ protected Change insert(String repoName, ChangeInserter ins) throws Exception {
+ return insert(repoName, ins, null, TimeUtil.now());
+ }
+
+ @CanIgnoreReturnValue
protected Change insert(
- TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner, Instant createdOn)
+ String repoName, ChangeInserter ins, @Nullable Account.Id owner, Instant createdOn)
throws Exception {
- Project.NameKey project =
- Project.nameKey(repo.getRepository().getDescription().getRepositoryName());
+ Project.NameKey project = Project.nameKey(repoName);
Account.Id ownerId = owner != null ? owner : userId;
IdentifiedUser user = userFactory.create(ownerId);
try (BatchUpdate bu = updateFactory.create(project, user, createdOn)) {
@@ -4092,34 +4196,36 @@
}
protected Change newPatchSet(
- TestRepository<Repo> repo, Change c, CurrentUser user, Optional<String> message)
- throws Exception {
- // Add a new file so the patch set is not a trivial rebase, to avoid default
- // Code-Review label copying.
- int n = c.currentPatchSetId().get() + 1;
- RevCommit commit =
- repo.parseBody(
- repo.commit()
- .message(message.orElse("message"))
- .add("file" + n, "contents " + n)
- .create());
+ String repoName, Change c, CurrentUser user, Optional<String> message) throws Exception {
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) {
+ // Add a new file so the patch set is not a trivial rebase, to avoid default
+ // Code-Review label copying.
+ int n = c.currentPatchSetId().get() + 1;
+ RevCommit commit =
+ repo.parseBody(
+ repo.commit()
+ .message(message.orElse("updated message"))
+ .add("file" + n, "contents " + n)
+ .create());
- PatchSetInserter inserter =
- patchSetFactory
- .create(changeNotesFactory.createChecked(c), PatchSet.id(c.getId(), n), commit)
- .setFireRevisionCreated(false)
- .setValidate(false);
- try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.now());
- ObjectInserter oi = repo.getRepository().newObjectInserter();
- ObjectReader reader = oi.newReader();
- RevWalk rw = new RevWalk(reader)) {
- bu.setRepository(repo.getRepository(), rw, oi);
- bu.setNotify(NotifyResolver.Result.none());
- bu.addOp(c.getId(), inserter);
- bu.execute();
+ PatchSetInserter inserter =
+ patchSetFactory
+ .create(changeNotesFactory.createChecked(c), PatchSet.id(c.getId(), n), commit)
+ .setFireRevisionCreated(false)
+ .setValidate(false);
+ try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.now());
+ ObjectInserter oi = repo.getRepository().newObjectInserter();
+ ObjectReader reader = oi.newReader();
+ RevWalk rw = new RevWalk(reader)) {
+ bu.setRepository(repo.getRepository(), rw, oi);
+ bu.setNotify(NotifyResolver.Result.none());
+ bu.addOp(c.getId(), inserter);
+ bu.execute();
+ }
+
+ return inserter.getChange();
}
-
- return inserter.getChange();
}
protected ThrowableSubject assertThatQueryException(Object query) throws Exception {
@@ -4144,17 +4250,27 @@
}
}
- protected TestRepository<Repo> createProject(String name) throws Exception {
- gApi.projects().create(name).get();
+ @CanIgnoreReturnValue
+ protected TestRepository<Repository> createAndOpenProject(String name) throws Exception {
+ createProject(name);
return new TestRepository<>(repoManager.openRepository(Project.nameKey(name)));
}
- protected TestRepository<Repo> createProject(String name, String parent) throws Exception {
+ protected TestRepository<Repository> createAndOpenProject(String name, String parent)
+ throws Exception {
+ createProject(name, parent);
+ return new TestRepository<>(repoManager.openRepository(Project.nameKey(name)));
+ }
+
+ protected void createProject(String name) throws Exception {
+ gApi.projects().create(name).get();
+ }
+
+ protected void createProject(String name, String parent) throws Exception {
ProjectInput input = new ProjectInput();
input.name = name;
input.parent = parent;
gApi.projects().create(input).get();
- return new TestRepository<>(repoManager.openRepository(Project.nameKey(name)));
}
protected QueryRequest newQuery(Object query) {
@@ -4365,4 +4481,12 @@
protected Schema<ChangeData> getSchema() {
return indexes.getSearchIndex().getSchema();
}
+
+ protected ChangeUpdate newUpdate(Change c) throws Exception {
+ ChangeUpdate update =
+ TestChanges.newUpdate(injector, c, Optional.empty(), /* shouldExist= */ true);
+ update.setPatchSetId(c.currentPatchSetId());
+ update.setAllowWriteToNewRef(true);
+ return update;
+ }
}
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
index fe60119..6b17bb6 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -27,14 +27,13 @@
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
/**
@@ -58,19 +57,21 @@
@UseClockStep
public void stopQueryIfNoMoreResults() throws Exception {
// create 2 visible changes
- TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
- insert(testRepo, newChange(testRepo));
- insert(testRepo, newChange(testRepo));
+ try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ }
// create 2 invisible changes
- TestRepository<Repo> hiddenProject = createProject("hiddenProject");
- insert(hiddenProject, newChange(hiddenProject));
- insert(hiddenProject, newChange(hiddenProject));
- projectOperations
- .project(Project.nameKey("hiddenProject"))
- .forUpdate()
- .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
- .update();
+ try (TestRepository<Repository> hiddenProject = createAndOpenProject("hiddenProject")) {
+ insert("hiddenProject", newChange(hiddenProject));
+ insert("hiddenProject", newChange(hiddenProject));
+ projectOperations
+ .project(Project.nameKey("hiddenProject"))
+ .forUpdate()
+ .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ }
AbstractFakeIndex<?, ?, ?> idx =
(AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
@@ -83,12 +84,13 @@
@Test
@UseClockStep
public void noLimitQueryPaginates() throws Exception {
- TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
- // create 4 changes
- insert(testRepo, newChange(testRepo));
- insert(testRepo, newChange(testRepo));
- insert(testRepo, newChange(testRepo));
- insert(testRepo, newChange(testRepo));
+ try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
+ // create 4 changes
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ }
// Set queryLimit to 2
projectOperations
@@ -111,11 +113,12 @@
@UseClockStep
public void internalQueriesPaginate() throws Exception {
// create 4 changes
- TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
- insert(testRepo, newChange(testRepo));
- insert(testRepo, newChange(testRepo));
- insert(testRepo, newChange(testRepo));
- insert(testRepo, newChange(testRepo));
+ try (TestRepository<Repository> testRepo = createAndOpenProject("repo")) {
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ insert("repo", newChange(testRepo));
+ }
// Set queryLimit to 2
projectOperations
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index 9717bfb..5cae012 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -24,11 +24,9 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
-import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
@@ -45,11 +43,11 @@
@Test
public void fullTextWithSpecialChars() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("foo_bar_foo").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("one.two.three").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
assertQuery("message:foo_ba");
assertQuery("message:bar", change1);
@@ -63,8 +61,8 @@
@Test
@Override
public void byOwnerInvalidQuery() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
String nameEmail = user.asIdentifiedUser().getNameEmail();
BadRequestException thrown =
@@ -76,17 +74,17 @@
@Test
public void openAndClosedChanges() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
// create 3 closed changes
- Change change1 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
- Change change2 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
- Change change3 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change1 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change2 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change3 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
// create 3 new changes
- Change change4 = insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
- Change change5 = insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
- Change change6 = insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
+ Change change4 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
+ Change change5 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
+ Change change6 = insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
// Set queryLimit to 1
projectOperations
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 540416f..12bafd5 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -401,9 +401,8 @@
String query = "uuid:" + uuid;
assertQuery(query, group);
- for (GroupIndex index : groupIndexes.getWriteIndexes()) {
- index.delete(uuid);
- }
+ deleteGroup(uuid);
+
assertQuery(query);
}
@@ -441,6 +440,10 @@
return createGroupWithDescription(name, null, members);
}
+ protected GroupInfo createGroup(GroupInput in) throws Exception {
+ return gApi.groups().create(in).get();
+ }
+
protected GroupInfo createGroupWithDescription(
String name, String description, AccountInfo... members) throws Exception {
GroupInput in = new GroupInput();
@@ -448,21 +451,27 @@
in.description = description;
in.members =
Arrays.asList(members).stream().map(a -> String.valueOf(a._accountId)).collect(toList());
- return gApi.groups().create(in).get();
+ return createGroup(in);
}
protected GroupInfo createGroupWithOwner(String name, GroupInfo ownerGroup) throws Exception {
GroupInput in = new GroupInput();
in.name = name;
in.ownerId = ownerGroup.id;
- return gApi.groups().create(in).get();
+ return createGroup(in);
}
protected GroupInfo createGroupThatIsVisibleToAll(String name) throws Exception {
GroupInput in = new GroupInput();
in.name = name;
in.visibleToAll = true;
- return gApi.groups().create(in).get();
+ return createGroup(in);
+ }
+
+ protected void deleteGroup(AccountGroup.UUID uuid) throws Exception {
+ for (GroupIndex index : groupIndexes.getWriteIndexes()) {
+ index.delete(uuid);
+ }
}
protected GroupInfo getGroup(AccountGroup.UUID uuid) throws Exception {
diff --git a/modules/jgit b/modules/jgit
index e74f385..a190130 160000
--- a/modules/jgit
+++ b/modules/jgit
@@ -1 +1 @@
-Subproject commit e74f3855ad9d54c986d60b0b2ea4c223d52b2cd1
+Subproject commit a1901305b26ed5e0116f138bc02837713d2cf5c3
diff --git a/plugins/BUILD b/plugins/BUILD
index 32efa3e..39560c5 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -6,7 +6,7 @@
"CORE_PLUGINS",
"CUSTOM_PLUGINS",
)
-load("@npm//@bazel/typescript:index.bzl", "ts_config")
+load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_test")
package(default_visibility = ["//visibility:public"])
@@ -169,3 +169,32 @@
pkgs = ["com.google.gerrit"],
title = "Gerrit Review Plugin API Documentation",
)
+
+# This is a generic test target for TypeScript plugins.
+#
+# `nodejs_test` needs to run in the directory where the `package.json` and
+# `node_modules` are, so unfortunately we cannot move this target into the
+# BUILD files of individual plugins. On the other hand one common target
+# for all plugins also has the advantage of being re-usable.
+#
+# For making this work for a specific plugin you have make the source files
+# of the plugin available as a `filegroup` and add it to the `data` attribute.
+# And you have to specify the `PLUGIN_DIR` in the `env` attribute.
+nodejs_test(
+ name = "web-test-runner",
+ size = "large",
+ chdir = package_name(),
+ data = [
+ ":package.json",
+ ":web-test-runner.config.mjs",
+ # This is an example of how you could reference your plugin sources:
+ # "//plugins/codemirror-editor/web:codemirror-test-sources",
+ "@plugins_npm//:node_modules",
+ ],
+ entry_point = "@plugins_npm//:node_modules/@web/test-runner/dist/bin.js",
+ env = {"PLUGIN_DIR": "codemirror-editor"},
+ tags = [
+ "local",
+ "manual",
+ ],
+)
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 3af12c5..c964b31 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 3af12c5a5e65861830b42bd07933e275c33b9159
+Subproject commit c964b31675de90cfafe43fff9ec357f4b97d1e08
diff --git a/plugins/package.json b/plugins/package.json
index 79bb7665..331a417 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -7,9 +7,14 @@
"@polymer/decorators": "^3.0.0",
"@polymer/polymer": "^3.4.1",
"@open-wc/testing": "^3.1.6",
+ "@types/codemirror": "^5.60.5",
+ "@web/dev-server-esbuild": "^0.3.2",
+ "@web/test-runner": "^0.14.0",
+ "codemirror": "^5.65.6",
"lit": "^2.2.3",
- "rxjs": "^6.6.7"
+ "rxjs": "^6.6.7",
+ "sinon": "^13.0.0"
},
"license": "Apache-2.0",
"private": true
-}
+}
\ No newline at end of file
diff --git a/plugins/replication b/plugins/replication
index 47ee3da..8fd3c27 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 47ee3dab0dd96900e85662adf0d5f48a33d17733
+Subproject commit 8fd3c271ce0a21480e3d04da5ad2112efea3bedf
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index ec05cad..368b3e0 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -23,6 +23,11 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
+"@esbuild/linux-loong64@0.14.54":
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
+ integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
+
"@esm-bundle/chai@^4.3.4-fix.0":
version "4.3.4-fix.0"
resolved "https://registry.yarnpkg.com/@esm-bundle/chai/-/chai-4.3.4-fix.0.tgz#3084cff7eb46d741749f47f3a48dbbdcbaf30a92"
@@ -40,6 +45,11 @@
resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.4.1.tgz#3f587eec5708692135bc9e94cf396130604979f3"
integrity sha512-qDv4851VFSaBWzpS02cXHclo40jsbAjRXnebNXpm0uVg32kCneZPo9RYVQtrTNICtZ+1wAYHu1ZtxWSWMbKrBw==
+"@mdn/browser-compat-data@^4.0.0":
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz#1fead437f3957ceebe2e8c3f46beccdb9bc575b8"
+ integrity sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA==
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -132,6 +142,69 @@
dependencies:
"@webcomponents/shadycss" "^1.9.1"
+"@rollup/plugin-node-resolve@^13.0.4":
+ version "13.3.0"
+ resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz#da1c5c5ce8316cef96a2f823d111c1e4e498801c"
+ integrity sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==
+ dependencies:
+ "@rollup/pluginutils" "^3.1.0"
+ "@types/resolve" "1.17.1"
+ deepmerge "^4.2.2"
+ is-builtin-module "^3.1.0"
+ is-module "^1.0.0"
+ resolve "^1.19.0"
+
+"@rollup/pluginutils@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
+ integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
+ dependencies:
+ "@types/estree" "0.0.39"
+ estree-walker "^1.0.1"
+ picomatch "^2.2.2"
+
+"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3":
+ version "1.8.6"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9"
+ integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==
+ dependencies:
+ type-detect "4.0.8"
+
+"@sinonjs/commons@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
+ integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==
+ dependencies:
+ type-detect "4.0.8"
+
+"@sinonjs/fake-timers@^10.0.2":
+ version "10.0.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c"
+ integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==
+ dependencies:
+ "@sinonjs/commons" "^2.0.0"
+
+"@sinonjs/fake-timers@^9.1.2":
+ version "9.1.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c"
+ integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==
+ dependencies:
+ "@sinonjs/commons" "^1.7.0"
+
+"@sinonjs/samsam@^6.1.1":
+ version "6.1.3"
+ resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.3.tgz#4e30bcd4700336363302a7d72cbec9b9ab87b104"
+ integrity sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==
+ dependencies:
+ "@sinonjs/commons" "^1.6.0"
+ lodash.get "^4.4.2"
+ type-detect "^4.0.8"
+
+"@sinonjs/text-encoding@^0.7.1":
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918"
+ integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
+
"@types/accepts@*":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
@@ -172,6 +245,18 @@
"@types/node" "*"
"@types/qs" "*"
+"@types/codemirror@^5.60.5":
+ version "5.60.5"
+ resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.5.tgz#5b989a3b4bbe657458cf372c92b6bfda6061a2b7"
+ integrity sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg==
+ dependencies:
+ "@types/tern" "*"
+
+"@types/command-line-args@^5.0.0":
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.0.tgz#adbb77980a1cc376bb208e3f4142e907410430f6"
+ integrity sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA==
+
"@types/connect@*":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
@@ -204,6 +289,16 @@
resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.1.tgz#79b65710bc8b6d44094d286aecf38e44f9627852"
integrity sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==
+"@types/estree@*":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
+ integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
+
+"@types/estree@0.0.39":
+ version "0.0.39"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
+ integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
+
"@types/express-serve-static-core@^4.17.18":
version "4.17.31"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f"
@@ -233,7 +328,7 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==
-"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.3":
+"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.3":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==
@@ -283,6 +378,11 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
+"@types/mocha@^8.2.0":
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323"
+ integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==
+
"@types/node@*":
version "18.8.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c"
@@ -303,6 +403,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
+"@types/resolve@1.17.1":
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
+ integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
+ dependencies:
+ "@types/node" "*"
+
"@types/serve-static@*":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155"
@@ -331,6 +438,13 @@
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e"
integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==
+"@types/tern@*":
+ version "0.23.4"
+ resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.4.tgz#03926eb13dbeaf3ae0d390caf706b2643a0127fb"
+ integrity sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==
+ dependencies:
+ "@types/estree" "*"
+
"@types/trusted-types@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
@@ -343,14 +457,28 @@
dependencies:
"@types/node" "*"
-"@web/browser-logs@^0.2.1":
+"@types/yauzl@^2.9.1":
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
+ integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
+ dependencies:
+ "@types/node" "*"
+
+"@web/browser-logs@^0.2.1", "@web/browser-logs@^0.2.2":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@web/browser-logs/-/browser-logs-0.2.5.tgz#0895efb641eacb0fbc1138c6092bd18c01df2734"
integrity sha512-Qxo1wY/L7yILQqg0jjAaueh+tzdORXnZtxQgWH23SsTCunz9iq9FvsZa8Q5XlpjnZ3vLIsFEuEsCMqFeohJnEg==
dependencies:
errorstacks "^2.2.0"
-"@web/dev-server-core@^0.3.18":
+"@web/config-loader@^0.1.3":
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/@web/config-loader/-/config-loader-0.1.3.tgz#8325ea54f75ef2ee7166783e64e66936db25bff7"
+ integrity sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==
+ dependencies:
+ semver "^7.3.4"
+
+"@web/dev-server-core@^0.3.18", "@web/dev-server-core@^0.3.19":
version "0.3.19"
resolved "https://registry.yarnpkg.com/@web/dev-server-core/-/dev-server-core-0.3.19.tgz#b61f9a0b92351371347a758b30ba19e683c72e94"
integrity sha512-Q/Xt4RMVebLWvALofz1C0KvP8qHbzU1EmdIA2Y1WMPJwiFJFhPxdr75p9YxK32P2t0hGs6aqqS5zE0HW9wYzYA==
@@ -374,6 +502,49 @@
picomatch "^2.2.2"
ws "^7.4.2"
+"@web/dev-server-esbuild@^0.3.2":
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-esbuild/-/dev-server-esbuild-0.3.3.tgz#e82af2e5acec0e645b920400be9601601b3921c5"
+ integrity sha512-hB9C8X9NsFWUG2XKT3W+Xcw3IZ/VObf4LNbK14BTjApnNyZfV6hVhSlJfvhgOoJ4DxsImfhIB5+gMRKOG9NmBw==
+ dependencies:
+ "@mdn/browser-compat-data" "^4.0.0"
+ "@web/dev-server-core" "^0.3.19"
+ esbuild "^0.12 || ^0.13 || ^0.14"
+ parse5 "^6.0.1"
+ ua-parser-js "^1.0.2"
+
+"@web/dev-server-rollup@^0.3.19":
+ version "0.3.19"
+ resolved "https://registry.yarnpkg.com/@web/dev-server-rollup/-/dev-server-rollup-0.3.19.tgz#188f3a37bcc38f4dc1b208663b14ab2d17321a57"
+ integrity sha512-IwiwI+fyX0YuvAOldStlYJ+Zm/JfSCk9OSGIs7+fWbOYysEHwkEVvBwoPowaclSZA44Tobvqt+6ej9udbbZ/WQ==
+ dependencies:
+ "@rollup/plugin-node-resolve" "^13.0.4"
+ "@web/dev-server-core" "^0.3.19"
+ nanocolors "^0.2.1"
+ parse5 "^6.0.1"
+ rollup "^2.67.0"
+ whatwg-url "^11.0.0"
+
+"@web/dev-server@^0.1.35":
+ version "0.1.35"
+ resolved "https://registry.yarnpkg.com/@web/dev-server/-/dev-server-0.1.35.tgz#d845822d7c3c7749adf03f7abac4a69e2a4490cc"
+ integrity sha512-E7TSTSFdGPzhkiE3kIVt8i49gsiAYpJIZHzs1vJmVfdt8U4rsmhE+5roezxZo0hkEw4mNsqj9zCc4Dzqy/IFHg==
+ dependencies:
+ "@babel/code-frame" "^7.12.11"
+ "@types/command-line-args" "^5.0.0"
+ "@web/config-loader" "^0.1.3"
+ "@web/dev-server-core" "^0.3.19"
+ "@web/dev-server-rollup" "^0.3.19"
+ camelcase "^6.2.0"
+ command-line-args "^5.1.1"
+ command-line-usage "^6.1.1"
+ debounce "^1.2.0"
+ deepmerge "^4.2.2"
+ ip "^1.1.5"
+ nanocolors "^0.2.1"
+ open "^8.0.2"
+ portfinder "^1.0.32"
+
"@web/parse5-utils@^1.2.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@web/parse5-utils/-/parse5-utils-1.3.0.tgz#e2e9e98b31a4ca948309f74891bda8d77399f6bd"
@@ -382,7 +553,17 @@
"@types/parse5" "^6.0.1"
parse5 "^6.0.1"
-"@web/test-runner-commands@^0.6.1":
+"@web/test-runner-chrome@^0.10.7":
+ version "0.10.7"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-chrome/-/test-runner-chrome-0.10.7.tgz#2dc35da47aa8b98c59f9e229a70ea3f443303e0c"
+ integrity sha512-DKJVHhHh3e/b6/erfKOy0a4kGfZ47qMoQRgROxi9T4F9lavEY3E5/MQ7hapHFM2lBF4vDrm+EWjtBdOL8o42tw==
+ dependencies:
+ "@web/test-runner-core" "^0.10.20"
+ "@web/test-runner-coverage-v8" "^0.4.8"
+ chrome-launcher "^0.15.0"
+ puppeteer-core "^13.1.3"
+
+"@web/test-runner-commands@^0.6.1", "@web/test-runner-commands@^0.6.3":
version "0.6.5"
resolved "https://registry.yarnpkg.com/@web/test-runner-commands/-/test-runner-commands-0.6.5.tgz#69a2a06b52fd9d329f9cf1e172cd8fb1d5ffc521"
integrity sha512-W+wLg10jEAJY9N6tNWqG1daKmAzxGmTbO/H9fFfcgOgdxdn+hHiR4r2/x1iylKbFLujHUQlnjNQeu2d6eDPFqg==
@@ -390,7 +571,7 @@
"@web/test-runner-core" "^0.10.27"
mkdirp "^1.0.4"
-"@web/test-runner-core@^0.10.27":
+"@web/test-runner-core@^0.10.20", "@web/test-runner-core@^0.10.27":
version "0.10.27"
resolved "https://registry.yarnpkg.com/@web/test-runner-core/-/test-runner-core-0.10.27.tgz#8d1430f2364fb36b3ac15b9b43034fae9d94e177"
integrity sha512-ClV/hSxs4wDm/ANFfQOdRRFb/c0sYywC1QfUXG/nS4vTp3nnt7x7mjydtMGGLmvK9f6Zkubkc1aa+7ryfmVwNA==
@@ -422,6 +603,46 @@
picomatch "^2.2.2"
source-map "^0.7.3"
+"@web/test-runner-coverage-v8@^0.4.8":
+ version "0.4.9"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.4.9.tgz#334d80cd19fc68c08ec3339b1b1d2725078b51a2"
+ integrity sha512-y9LWL4uY25+fKQTljwr0XTYjeWIwU4h8eYidVuLoW3n1CdFkaddv+smrGzzF5j8XY+Mp6TmV9NdxjvMWqVkDdw==
+ dependencies:
+ "@web/test-runner-core" "^0.10.20"
+ istanbul-lib-coverage "^3.0.0"
+ picomatch "^2.2.2"
+ v8-to-istanbul "^8.0.0"
+
+"@web/test-runner-mocha@^0.7.5":
+ version "0.7.5"
+ resolved "https://registry.yarnpkg.com/@web/test-runner-mocha/-/test-runner-mocha-0.7.5.tgz#696f8cb7f5118a72bd7ac5778367ae3bd3fb92cd"
+ integrity sha512-12/OBq6efPCAvJpcz3XJs2OO5nHe7GtBibZ8Il1a0QtsGpRmuJ4/m1EF0Fj9f6KHg7JdpGo18A37oE+5hXjHwg==
+ dependencies:
+ "@types/mocha" "^8.2.0"
+ "@web/test-runner-core" "^0.10.20"
+
+"@web/test-runner@^0.14.0":
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/@web/test-runner/-/test-runner-0.14.1.tgz#a637e45c9b6ce7860ab780b5ac82dbfa1ed824f9"
+ integrity sha512-S2/Xp/bZBJdbWeTQxhs45cO9Khwqx99X+rrx8l0uDR0Ju/+kX+yC3RpjnOY1ooKD3rjkoEAE82soZTZNz+aKIg==
+ dependencies:
+ "@web/browser-logs" "^0.2.2"
+ "@web/config-loader" "^0.1.3"
+ "@web/dev-server" "^0.1.35"
+ "@web/test-runner-chrome" "^0.10.7"
+ "@web/test-runner-commands" "^0.6.3"
+ "@web/test-runner-core" "^0.10.27"
+ "@web/test-runner-mocha" "^0.7.5"
+ camelcase "^6.2.0"
+ command-line-args "^5.1.1"
+ command-line-usage "^6.1.1"
+ convert-source-map "^1.7.0"
+ diff "^5.0.0"
+ globby "^11.0.1"
+ nanocolors "^0.2.1"
+ portfinder "^1.0.32"
+ source-map "^0.7.3"
+
"@webcomponents/shadycss@^1.9.1":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.11.0.tgz#73e289996c002d8be694cd3be0e83c46ad25e7e0"
@@ -435,6 +656,13 @@
mime-types "~2.1.34"
negotiator "0.6.3"
+agent-base@6:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
+ integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
+ dependencies:
+ debug "4"
+
ansi-escapes@^4.3.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@@ -469,6 +697,16 @@
normalize-path "^3.0.0"
picomatch "^2.0.4"
+array-back@^3.0.1, array-back@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
+ integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
+
+array-back@^4.0.1, array-back@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e"
+ integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==
+
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@@ -479,16 +717,50 @@
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
+async@^2.6.4:
+ version "2.6.4"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
+ integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
+ dependencies:
+ lodash "^4.17.14"
+
axe-core@^4.3.3:
version "4.4.3"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base64-js@^1.3.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+bl@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
+ integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
+ dependencies:
+ buffer "^5.5.0"
+ inherits "^2.0.4"
+ readable-stream "^3.4.0"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@@ -496,6 +768,24 @@
dependencies:
fill-range "^7.0.1"
+buffer-crc32@~0.2.3:
+ version "0.2.13"
+ resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+ integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
+
+buffer@^5.2.1, buffer@^5.5.0:
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+ integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.1.13"
+
+builtin-modules@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
+ integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
+
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@@ -517,6 +807,11 @@
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
+camelcase@^6.2.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+ integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
chai-a11y-axe@^1.3.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/chai-a11y-axe/-/chai-a11y-axe-1.4.0.tgz#e584af967727a8656e27c32e845f5db21f2bf2e0"
@@ -524,7 +819,7 @@
dependencies:
axe-core "^4.3.3"
-chalk@^2.0.0:
+chalk@^2.0.0, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -548,6 +843,21 @@
optionalDependencies:
fsevents "~2.3.2"
+chownr@^1.1.1:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+ integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+chrome-launcher@^0.15.0:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.1.tgz#0a0208037063641e2b3613b7e42b0fcb3fa2d399"
+ integrity sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg==
+ dependencies:
+ "@types/node" "*"
+ escape-string-regexp "^4.0.0"
+ is-wsl "^2.2.0"
+ lighthouse-logger "^1.0.0"
+
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@@ -575,6 +885,11 @@
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==
+codemirror@^5.65.6:
+ version "5.65.10"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.10.tgz#4276a93b8534ce91f14b733ba9a1ac949666eac9"
+ integrity sha512-IXAG5wlhbgcTJ6rZZcmi4+sjWIbJqIGfeg3tNa3yX84Jb3T4huS5qzQAo/cUisc1l3bI47WZodpyf7cYcocDKg==
+
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -599,6 +914,31 @@
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+command-line-args@^5.1.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e"
+ integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==
+ dependencies:
+ array-back "^3.1.0"
+ find-replace "^3.0.0"
+ lodash.camelcase "^4.3.0"
+ typical "^4.0.0"
+
+command-line-usage@^6.1.1:
+ version "6.1.3"
+ resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957"
+ integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==
+ dependencies:
+ array-back "^4.0.2"
+ chalk "^2.4.2"
+ table-layout "^1.0.2"
+ typical "^5.2.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
content-disposition@~0.5.2:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
@@ -611,6 +951,11 @@
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+convert-source-map@^1.6.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
+ integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+
convert-source-map@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
@@ -626,30 +971,54 @@
depd "~2.0.0"
keygrip "~1.1.0"
+cross-fetch@3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
+ integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
+ dependencies:
+ node-fetch "2.6.7"
+
debounce@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
-debug@^3.1.0:
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
- integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
- dependencies:
- ms "^2.1.1"
-
-debug@^4.1.1, debug@^4.3.2:
+debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
+debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.1.0, debug@^3.2.7:
+ version "3.2.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+ integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+ dependencies:
+ ms "^2.1.1"
+
deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==
+deep-extend@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deepmerge@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+ integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+
define-lazy-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
@@ -680,6 +1049,16 @@
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+devtools-protocol@0.0.981744:
+ version "0.0.981744"
+ resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.981744.tgz#9960da0370284577d46c28979a0b32651022bacf"
+ integrity sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==
+
+diff@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
+ integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
+
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -702,6 +1081,13 @@
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+ integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+ dependencies:
+ once "^1.4.0"
+
errorstacks@^2.2.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/errorstacks/-/errorstacks-2.4.0.tgz#2155674dd9e741aacda3f3b8b967d9c40a4a0baf"
@@ -712,6 +1098,133 @@
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.0.3.tgz#f0d8d35b36d13024110000d5e6fadc8eeaeb66b8"
integrity sha512-iC67eXHToclrlVhQfpRawDiF8D8sQxNxmbqw5oebegOaJkyx/w9C/k57/5e6yJR2zIByRt9OXdqX50DV2t6ZKw==
+esbuild-android-64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
+ integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
+
+esbuild-android-arm64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
+ integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
+
+esbuild-darwin-64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
+ integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
+
+esbuild-darwin-arm64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73"
+ integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==
+
+esbuild-freebsd-64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
+ integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
+
+esbuild-freebsd-arm64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
+ integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
+
+esbuild-linux-32@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
+ integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
+
+esbuild-linux-64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652"
+ integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
+
+esbuild-linux-arm64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
+ integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
+
+esbuild-linux-arm@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
+ integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
+
+esbuild-linux-mips64le@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
+ integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
+
+esbuild-linux-ppc64le@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
+ integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
+
+esbuild-linux-riscv64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
+ integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
+
+esbuild-linux-s390x@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
+ integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
+
+esbuild-netbsd-64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
+ integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
+
+esbuild-openbsd-64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
+ integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
+
+esbuild-sunos-64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
+ integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
+
+esbuild-windows-32@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
+ integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
+
+esbuild-windows-64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
+ integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
+
+esbuild-windows-arm64@0.14.54:
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
+ integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
+
+"esbuild@^0.12 || ^0.13 || ^0.14":
+ version "0.14.54"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
+ integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
+ optionalDependencies:
+ "@esbuild/linux-loong64" "0.14.54"
+ esbuild-android-64 "0.14.54"
+ esbuild-android-arm64 "0.14.54"
+ esbuild-darwin-64 "0.14.54"
+ esbuild-darwin-arm64 "0.14.54"
+ esbuild-freebsd-64 "0.14.54"
+ esbuild-freebsd-arm64 "0.14.54"
+ esbuild-linux-32 "0.14.54"
+ esbuild-linux-64 "0.14.54"
+ esbuild-linux-arm "0.14.54"
+ esbuild-linux-arm64 "0.14.54"
+ esbuild-linux-mips64le "0.14.54"
+ esbuild-linux-ppc64le "0.14.54"
+ esbuild-linux-riscv64 "0.14.54"
+ esbuild-linux-s390x "0.14.54"
+ esbuild-netbsd-64 "0.14.54"
+ esbuild-openbsd-64 "0.14.54"
+ esbuild-sunos-64 "0.14.54"
+ esbuild-windows-32 "0.14.54"
+ esbuild-windows-64 "0.14.54"
+ esbuild-windows-arm64 "0.14.54"
+
escape-html@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -722,11 +1235,32 @@
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+estree-walker@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
+ integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
+
etag@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
+extract-zip@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
+ integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
+ dependencies:
+ debug "^4.1.1"
+ get-stream "^5.1.0"
+ yauzl "^2.10.0"
+ optionalDependencies:
+ "@types/yauzl" "^2.9.1"
+
fast-glob@^3.2.9:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
@@ -745,6 +1279,13 @@
dependencies:
reusify "^1.0.4"
+fd-slicer@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
+ integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
+ dependencies:
+ pend "~1.2.0"
+
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -752,11 +1293,36 @@
dependencies:
to-regex-range "^5.0.1"
+find-replace@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38"
+ integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==
+ dependencies:
+ array-back "^3.0.1"
+
+find-up@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
+fs-constants@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+ integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
@@ -776,6 +1342,13 @@
has "^1.0.3"
has-symbols "^1.0.3"
+get-stream@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
+ integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
+ dependencies:
+ pump "^3.0.0"
+
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@@ -788,6 +1361,18 @@
dependencies:
is-glob "^4.0.1"
+glob@^7.1.3:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
globby@^11.0.1:
version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
@@ -874,6 +1459,14 @@
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
+https-proxy-agent@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
+ integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
+ dependencies:
+ agent-base "6"
+ debug "4"
+
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -881,6 +1474,11 @@
dependencies:
safer-buffer ">= 2.1.2 < 3"
+ieee754@^1.1.13:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
ignore@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
@@ -891,16 +1489,24 @@
resolved "https://registry.yarnpkg.com/inflation/-/inflation-2.0.0.tgz#8b417e47c28f925a45133d914ca1fd389107f30f"
integrity sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
-inherits@2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
ip@^1.1.5:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48"
@@ -913,6 +1519,20 @@
dependencies:
binary-extensions "^2.0.0"
+is-builtin-module@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.0.tgz#bb0310dfe881f144ca83f30100ceb10cf58835e0"
+ integrity sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==
+ dependencies:
+ builtin-modules "^3.3.0"
+
+is-core-module@^2.9.0:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
+ integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+ dependencies:
+ has "^1.0.3"
+
is-docker@^2.0.0, is-docker@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
@@ -942,6 +1562,11 @@
dependencies:
is-extglob "^2.1.1"
+is-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+ integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==
+
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
@@ -959,6 +1584,11 @@
dependencies:
is-docker "^2.0.0"
+isarray@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+ integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
+
isbinaryfile@^4.0.6:
version "4.0.10"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3"
@@ -991,6 +1621,11 @@
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+just-extend@^4.0.2:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
+ integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==
+
keygrip@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
@@ -1064,6 +1699,14 @@
type-is "^1.6.16"
vary "^1.1.2"
+lighthouse-logger@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.3.0.tgz#ba6303e739307c4eee18f08249524e7dafd510db"
+ integrity sha512-BbqAKApLb9ywUli+0a+PcV04SyJ/N1q/8qgCNe6U97KbPCS1BTksEuHFLYdvc8DltuhfxIUBqDZsC0bBGtl3lA==
+ dependencies:
+ debug "^2.6.9"
+ marky "^1.2.2"
+
lit-element@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.2.2.tgz#d148ab6bf4c53a33f707a5168e087725499e5f2b"
@@ -1088,6 +1731,28 @@
lit-element "^3.2.0"
lit-html "^2.4.0"
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+lodash.camelcase@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+ integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
+
+lodash.get@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+ integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
+
+lodash@^4.17.14:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
log-update@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
@@ -1112,6 +1777,11 @@
dependencies:
semver "^6.0.0"
+marky@^1.2.2:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0"
+ integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -1147,11 +1817,40 @@
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+minimatch@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.2.6:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
+ integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
+
+mkdirp-classic@^0.5.2:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
+ integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+
+mkdirp@^0.5.6:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+ integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+ dependencies:
+ minimist "^1.2.6"
+
mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
+
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@@ -1177,6 +1876,24 @@
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+nise@^5.1.1:
+ version "5.1.4"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0"
+ integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==
+ dependencies:
+ "@sinonjs/commons" "^2.0.0"
+ "@sinonjs/fake-timers" "^10.0.2"
+ "@sinonjs/text-encoding" "^0.7.1"
+ just-extend "^4.0.2"
+ path-to-regexp "^1.7.0"
+
+node-fetch@2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+ integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
+ dependencies:
+ whatwg-url "^5.0.0"
+
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@@ -1194,6 +1911,13 @@
dependencies:
ee-first "1.1.1"
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
onetime@^5.1.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
@@ -1215,6 +1939,25 @@
is-docker "^2.1.1"
is-wsl "^2.2.0"
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
parse5@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
@@ -1225,21 +1968,100 @@
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-path-is-absolute@1.0.1:
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@1.0.1, path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-to-regexp@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
+ integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
+ dependencies:
+ isarray "0.0.1"
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+pend@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+ integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
+
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+pkg-dir@4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
+portfinder@^1.0.32:
+ version "1.0.32"
+ resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81"
+ integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==
+ dependencies:
+ async "^2.6.4"
+ debug "^3.2.7"
+ mkdirp "^0.5.6"
+
+progress@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+ integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
+proxy-from-env@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+punycode@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+puppeteer-core@^13.1.3:
+ version "13.7.0"
+ resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-13.7.0.tgz#3344bee3994163f49120a55ddcd144a40575ba5b"
+ integrity sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q==
+ dependencies:
+ cross-fetch "3.1.5"
+ debug "4.3.4"
+ devtools-protocol "0.0.981744"
+ extract-zip "2.0.1"
+ https-proxy-agent "5.0.1"
+ pkg-dir "4.2.0"
+ progress "2.0.3"
+ proxy-from-env "1.1.0"
+ rimraf "3.0.2"
+ tar-fs "2.1.1"
+ unbzip2-stream "1.4.3"
+ ws "8.5.0"
+
qs@^6.5.2:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
@@ -1262,6 +2084,15 @@
iconv-lite "0.4.24"
unpipe "1.0.0"
+readable-stream@^3.1.1, readable-stream@^3.4.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -1269,6 +2100,11 @@
dependencies:
picomatch "^2.2.1"
+reduce-flatten@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27"
+ integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==
+
resolve-path@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
@@ -1277,6 +2113,15 @@
http-errors "~1.6.2"
path-is-absolute "1.0.1"
+resolve@^1.19.0:
+ version "1.22.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
+ integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+ dependencies:
+ is-core-module "^2.9.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
@@ -1290,6 +2135,20 @@
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+rimraf@3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
+rollup@^2.67.0:
+ version "2.79.1"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
+ integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
+ optionalDependencies:
+ fsevents "~2.3.2"
+
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -1304,7 +2163,7 @@
dependencies:
tslib "^1.9.0"
-safe-buffer@5.2.1:
+safe-buffer@5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -1324,6 +2183,13 @@
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+semver@^7.3.4:
+ version "7.3.8"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
+ integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
+ dependencies:
+ lru-cache "^6.0.0"
+
setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
@@ -1348,6 +2214,18 @@
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+sinon@^13.0.0:
+ version "13.0.2"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-13.0.2.tgz#c6a8ddd655dc1415bbdc5ebf0e5b287806850c3a"
+ integrity sha512-KvOrztAVqzSJWMDoxM4vM+GPys1df2VBoXm+YciyB/OLMamfS3VXh3oGh5WtrAGSzrgczNWFFY22oKb7Fi5eeA==
+ dependencies:
+ "@sinonjs/commons" "^1.8.3"
+ "@sinonjs/fake-timers" "^9.1.2"
+ "@sinonjs/samsam" "^6.1.1"
+ diff "^5.0.0"
+ nise "^5.1.1"
+ supports-color "^7.2.0"
+
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
@@ -1386,6 +2264,13 @@
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -1400,13 +2285,54 @@
dependencies:
has-flag "^3.0.0"
-supports-color@^7.1.0:
+supports-color@^7.1.0, supports-color@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+table-layout@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04"
+ integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==
+ dependencies:
+ array-back "^4.0.1"
+ deep-extend "~0.6.0"
+ typical "^5.2.0"
+ wordwrapjs "^4.0.0"
+
+tar-fs@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
+ integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
+ dependencies:
+ chownr "^1.1.1"
+ mkdirp-classic "^0.5.2"
+ pump "^3.0.0"
+ tar-stream "^2.1.4"
+
+tar-stream@^2.1.4:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+ integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+ dependencies:
+ bl "^4.0.3"
+ end-of-stream "^1.4.1"
+ fs-constants "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^3.1.1"
+
+through@^2.3.8:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+ integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
+
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -1419,6 +2345,18 @@
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+tr46@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
+ integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
+ dependencies:
+ punycode "^2.1.1"
+
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+ integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -1429,6 +2367,11 @@
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
+type-detect@4.0.8, type-detect@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
+ integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
+
type-fest@^0.21.3:
version "0.21.3"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
@@ -1442,16 +2385,87 @@
media-typer "0.3.0"
mime-types "~2.1.24"
+typical@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
+ integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
+
+typical@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
+ integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
+
+ua-parser-js@^1.0.2:
+ version "1.0.32"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.32.tgz#786bf17df97de159d5b1c9d5e8e9e89806f8a030"
+ integrity sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==
+
+unbzip2-stream@1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
+ integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
+ dependencies:
+ buffer "^5.2.1"
+ through "^2.3.8"
+
unpipe@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
+util-deprecate@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+v8-to-istanbul@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed"
+ integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.1"
+ convert-source-map "^1.6.0"
+ source-map "^0.7.3"
+
vary@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+ integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+webidl-conversions@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
+ integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
+
+whatwg-url@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
+ integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==
+ dependencies:
+ tr46 "^3.0.0"
+ webidl-conversions "^7.0.0"
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+ integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
+wordwrapjs@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f"
+ integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==
+ dependencies:
+ reduce-flatten "^2.0.0"
+ typical "^5.2.0"
+
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -1461,6 +2475,16 @@
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+ws@8.5.0:
+ version "8.5.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
+ integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
+
ws@^7.4.2:
version "7.5.9"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
@@ -1471,6 +2495,14 @@
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+yauzl@^2.10.0:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
+ integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
+ dependencies:
+ buffer-crc32 "~0.2.3"
+ fd-slicer "~1.1.0"
+
ylru@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785"
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index ce9ce2d..e3b3ad4 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -201,7 +201,7 @@
syntax_highlighting?: boolean;
tab_size: number;
font_size: number;
- // TODO: Missing documentation
+ // Hides the FILE and LOST diff rows. Default is TRUE.
show_file_comment_button?: boolean;
line_wrapping?: boolean;
}
diff --git a/polygerrit-ui/app/api/plugin.ts b/polygerrit-ui/app/api/plugin.ts
index 3ed48e6..d3d012d 100644
--- a/polygerrit-ui/app/api/plugin.ts
+++ b/polygerrit-ui/app/api/plugin.ts
@@ -78,8 +78,6 @@
moduleName?: string,
options?: RegisterOptions
): HookApi<T>;
- // DEPRECATED: Use styleApi() instead.
- registerStyleModule(endpoint: string, moduleName: string): void;
reporting(): ReportingPluginApi;
restApi(): RestPluginApi;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 89c7622..f915432 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -319,6 +319,4 @@
export const RELOAD_DASHBOARD_INTERVAL_MS = 10 * 1000;
-export const SHOWN_ITEMS_COUNT = 25;
-
export const WAITING = 'Waiting';
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
index c472d4d..893a997 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.ts
@@ -5,19 +5,21 @@
*/
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-list-view/gr-list-view';
-import '../../shared/gr-overlay/gr-overlay';
import '../gr-create-group-dialog/gr-create-group-dialog';
import {GroupId, GroupInfo, GroupName} from '../../../types/common';
import {GrCreateGroupDialog} from '../gr-create-group-dialog/gr-create-group-dialog';
import {fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
-import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {tableStyles} from '../../../styles/gr-table-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, query, property, state} from 'lit/decorators.js';
import {assertIsDefined} from '../../../utils/common-util';
-import {AdminViewState} from '../../../models/views/admin';
+import {
+ AdminChildView,
+ AdminViewState,
+ createAdminUrl,
+} from '../../../models/views/admin';
import {createGroupUrl} from '../../../models/views/group';
import {whenVisible} from '../../../utils/dom-util';
import {modalStyles} from '../../../styles/gr-modal-styles';
@@ -30,8 +32,6 @@
@customElement('gr-admin-group-list')
export class GrAdminGroupList extends LitElement {
- readonly path = '/admin/groups';
-
@query('#createModal') private createModal?: HTMLDialogElement;
@query('#createNewModal') private createNewModal?: GrCreateGroupDialog;
@@ -42,21 +42,19 @@
/**
* Offset of currently visible query results.
*/
- @state() private offset = 0;
+ @state() offset = 0;
- @state() private hasNewGroupName = false;
+ @state() hasNewGroupName = false;
- @state() private createNewCapability = false;
+ @state() createNewCapability = false;
- // private but used in test
@state() groups: GroupInfo[] = [];
- @state() private groupsPerPage = 25;
+ @state() groupsPerPage = 25;
- // private but used in test
@state() loading = true;
- @state() private filter = '';
+ @state() filter = '';
private readonly restApiService = getAppContext().restApiService;
@@ -88,7 +86,7 @@
.itemsPerPage=${this.groupsPerPage}
.loading=${this.loading}
.offset=${this.offset}
- .path=${this.path}
+ .path=${createAdminUrl({adminView: AdminChildView.GROUPS})}
@create-clicked=${() => this.handleCreateClicked()}
>
<table id="list" class="genericList">
@@ -107,7 +105,7 @@
</tbody>
<tbody class=${this.loading ? 'loading' : ''}>
${this.groups
- .slice(0, SHOWN_ITEMS_COUNT)
+ .slice(0, this.groupsPerPage)
.map(group => this.renderGroupList(group))}
</tbody>
</table>
@@ -168,18 +166,14 @@
*
* private but used in test
*/
- maybeOpenCreateModal(params?: AdminViewState) {
+ async maybeOpenCreateModal(params?: AdminViewState) {
if (params?.openCreateModal) {
- assertIsDefined(this.createModal, 'createModal');
- this.createModal.showModal();
+ await this.updateComplete;
+ if (!this.createModal?.open) this.createModal?.showModal();
}
}
- /**
- * Generates groups link (/admin/groups/<uuid>)
- *
- * private but used in test
- */
+ // private but used in test
computeGroupUrl(encodedId: string) {
const groupId = decodeURIComponent(encodedId) as GroupId;
return createGroupUrl({groupId});
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
index 1b128aa..fe5aa22 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.ts
@@ -15,7 +15,6 @@
import {GerritView} from '../../../services/router/router-model';
import {GrListView} from '../../shared/gr-list-view/gr-list-view';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
-import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {fixture, html, assert} from '@open-wc/testing';
import {AdminChildView, AdminViewState} from '../../../models/views/admin';
@@ -117,20 +116,22 @@
});
test('groups', () => {
- assert.equal(element.groups.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ const table = queryAndAssert(element, 'table');
+ const rows = table.querySelectorAll('tr.table');
+ assert.equal(rows.length, element.groupsPerPage);
});
- test('maybeOpenCreateModal', () => {
+ test('maybeOpenCreateModal', async () => {
const modalOpen = sinon.stub(
queryAndAssert<HTMLDialogElement>(element, '#createModal'),
'showModal'
);
- element.maybeOpenCreateModal();
+ await element.maybeOpenCreateModal();
assert.isFalse(modalOpen.called);
- element.maybeOpenCreateModal(undefined);
+ await element.maybeOpenCreateModal(undefined);
assert.isFalse(modalOpen.called);
value.openCreateModal = true;
- element.maybeOpenCreateModal(value);
+ await element.maybeOpenCreateModal(value);
assert.isTrue(modalOpen.called);
});
});
@@ -145,7 +146,9 @@
});
test('groups', () => {
- assert.equal(element.groups.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ const table = queryAndAssert(element, 'table');
+ const rows = table.querySelectorAll('tr.table');
+ assert.equal(rows.length, element.groupsPerPage);
});
});
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
index c3f79c7..0cfbaa4 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-file-edit-dialog.ts
@@ -14,7 +14,7 @@
import {LitElement, html, nothing} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {resolve} from '../../../models/dependency';
-import {createEditUrl} from '../../../models/views/edit';
+import {createEditUrl} from '../../../models/views/change';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {assertIsDefined} from '../../../utils/common-util';
import {when} from 'lit/directives/when.js';
@@ -162,8 +162,8 @@
const url = createEditUrl({
changeNum: change._number,
repo: change.project,
- path: this.path,
patchNum: 1 as PatchSetNumber,
+ editView: {path: this.path},
});
this.getNavigation().setUrl(url);
}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
index 4808d00..8d5689c 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.ts
@@ -6,9 +6,8 @@
import '@polymer/iron-input/iron-input';
import '../../../styles/gr-form-styles';
import '../../../styles/shared-styles';
-import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {page} from '../../../utils/page-wrapper-utils';
-import {GroupName} from '../../../types/common';
+import {GroupId, GroupName} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
import {formStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -16,6 +15,7 @@
import {customElement, query, property} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
import {fireEvent} from '../../../utils/event-util';
+import {createGroupUrl} from '../../../models/views/group';
declare global {
interface HTMLElementTagNameMap {
@@ -75,10 +75,6 @@
fireEvent(this, 'has-new-group-name');
}
- private computeGroupUrl(groupId: string) {
- return getBaseUrl() + '/admin/groups/' + encodeURL(groupId, true);
- }
-
override focus() {
this.input.focus();
}
@@ -89,7 +85,9 @@
if (groupRegistered.status !== 201) return;
return this.restApiService.getGroupConfig(name).then(group => {
if (!group) return;
- page.show(this.computeGroupUrl(String(group.group_id!)));
+ const groupId = String(group.group_id!) as GroupId;
+ // TODO: Use navigation service instead of `page.show()` directly.
+ page.show(createGroupUrl({groupId}));
});
});
}
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
index a90abbd..bb59ccc 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
@@ -7,7 +7,6 @@
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
-import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {page} from '../../../utils/page-wrapper-utils';
import {
BranchName,
@@ -24,6 +23,7 @@
import {customElement, query, property, state} from 'lit/decorators.js';
import {fireEvent} from '../../../utils/event-util';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {createRepoUrl} from '../../../models/views/repo';
declare global {
interface HTMLElementTagNameMap {
@@ -183,10 +183,6 @@
`;
}
- _computeRepoUrl(repoName: string) {
- return getBaseUrl() + '/admin/repos/' + encodeURL(repoName, true);
- }
-
override focus() {
this.input?.focus();
}
@@ -199,7 +195,8 @@
);
if (repoRegistered.status === 201) {
this.repoCreated = true;
- page.show(this._computeRepoUrl(this.repoConfig.name));
+ // TODO: Use navigation service instead of `page.show()` directly.
+ page.show(createRepoUrl({repo: this.repoConfig.name}));
}
return repoRegistered;
}
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
index 383b4a7..944cd7a 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.ts
@@ -9,7 +9,6 @@
import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
-import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {tableStyles} from '../../../styles/gr-table-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, css, html} from 'lit';
@@ -34,17 +33,15 @@
/**
* Offset of currently visible query results.
*/
- @state() private offset = 0;
+ @state() offset = 0;
- // private but used in test
@state() plugins?: PluginInfoWithName[];
- @state() private pluginsPerPage = 25;
+ @state() pluginsPerPage = 25;
- // private but used in test
@state() loading = true;
- @state() private filter = '';
+ @state() filter = '';
private readonly restApiService = getAppContext().restApiService;
@@ -107,7 +104,7 @@
return html`
<tbody>
${this.plugins
- ?.slice(0, SHOWN_ITEMS_COUNT)
+ ?.slice(0, this.pluginsPerPage)
.map(plugin => this.renderPluginList(plugin))}
</tbody>
`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts
index 4057e52..34c88be 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.ts
@@ -17,7 +17,6 @@
import {PluginInfo} from '../../../types/common';
import {GerritView} from '../../../services/router/router-model';
import {PageErrorEvent} from '../../../types/events';
-import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {fixture, html, assert} from '@open-wc/testing';
import {AdminChildView, AdminViewState} from '../../../models/views/admin';
@@ -334,7 +333,9 @@
});
test('plugins', () => {
- assert.equal(element.plugins!.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ const table = queryAndAssert(element, 'table');
+ const rows = table.querySelectorAll('tr.table');
+ assert.equal(rows.length, element.pluginsPerPage);
});
});
@@ -348,7 +349,9 @@
});
test('plugins', () => {
- assert.equal(element.plugins!.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ const table = queryAndAssert(element, 'table');
+ const rows = table.querySelectorAll('tr.table');
+ assert.equal(rows.length, element.pluginsPerPage);
});
});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 389e7c4..52e0b3f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '../gr-access-section/gr-access-section';
-import {encodeURL, getBaseUrl, singleDecodeURL} from '../../../utils/url-util';
+import {singleDecodeURL} from '../../../utils/url-util';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {toSortedPermissionsArray} from '../../../utils/access-util';
import {
@@ -44,6 +44,7 @@
import {resolve} from '../../../models/dependency';
import {createChangeUrl} from '../../../models/views/change';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {createRepoUrl, RepoDetailView} from '../../../models/views/repo';
const NOTHING_TO_SAVE = 'No changes to save.';
@@ -726,10 +727,10 @@
computeParentHref() {
if (!this.inheritsFrom?.name) return '';
- return `${getBaseUrl()}/admin/repos/${encodeURL(
- this.inheritsFrom.name,
- true
- )},access`;
+ return createRepoUrl({
+ repo: this.inheritsFrom.name,
+ detail: RepoDetailView.ACCESS,
+ });
}
private handleEditInheritFromTextChanged(e: ValueChangedEvent) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
index e13609e..11cfaab 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.ts
@@ -7,7 +7,6 @@
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-dialog/gr-dialog';
-import '../../shared/gr-overlay/gr-overlay';
import '../gr-create-change-dialog/gr-create-change-dialog';
import '../gr-create-change-dialog/gr-create-file-edit-dialog';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
@@ -32,7 +31,7 @@
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, query, property, state} from 'lit/decorators.js';
import {assertIsDefined} from '../../../utils/common-util';
-import {createEditUrl} from '../../../models/views/edit';
+import {createEditUrl} from '../../../models/views/change';
import {resolve} from '../../../models/dependency';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {GrCreateFileEditDialog} from '../gr-create-change-dialog/gr-create-file-edit-dialog';
@@ -321,8 +320,8 @@
createEditUrl({
changeNum: change._number,
repo: change.project,
- path: CONFIG_PATH,
patchNum: INITIAL_PATCHSET,
+ editView: {path: CONFIG_PATH},
})
);
})
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
index 3b193a6..981cbe4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts
@@ -25,7 +25,6 @@
import {firePageError} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
-import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {formStyles} from '../../../styles/gr-form-styles';
import {tableStyles} from '../../../styles/gr-table-styles';
import {sharedStyles} from '../../../styles/shared-styles';
@@ -51,36 +50,30 @@
@property({type: Object})
params?: RepoViewState;
- // private but used in test
@state() detailType?: RepoDetailView.BRANCHES | RepoDetailView.TAGS;
- // private but used in test
@state() isOwner = false;
- @state() private loggedIn = false;
+ @state() loggedIn = false;
- @state() private offset = 0;
+ @state() offset = 0;
- // private but used in test
@state() repo?: RepoName;
- // private but used in test
@state() items?: BranchInfo[] | TagInfo[];
- @state() private readonly itemsPerPage = 25;
+ @state() readonly itemsPerPage = 25;
- @state() private loading = true;
+ @state() loading = true;
- @state() private filter?: string;
+ @state() filter?: string;
- @state() private refName?: GitRef;
+ @state() refName?: GitRef;
- @state() private newItemName = false;
+ @state() newItemName = false;
- // private but used in test
@state() isEditing = false;
- // private but used in test
@state() revisedRef?: GitRef;
private readonly restApiService = getAppContext().restApiService;
@@ -185,7 +178,7 @@
</tbody>
<tbody class=${this.loading ? 'loading' : ''}>
${this.items
- ?.slice(0, SHOWN_ITEMS_COUNT)
+ ?.slice(0, this.itemsPerPage)
.map((item, index) => this.renderItemList(item, index))}
</tbody>
</table>
@@ -442,6 +435,9 @@
}
private getPath(repo?: RepoName, detailType?: RepoDetailView) {
+ // TODO: Replace with `createRepoUrl()`, but be aware that `encodeURL()`
+ // gets `false` as a second parameter here. The router pattern in gr-router
+ // does not handle the filter URLs, if the repo is not encoded!
return `/admin/repos/${encodeURL(repo ?? '', false)},${detailType}`;
}
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
index 28fc751..9b9cf29 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.ts
@@ -32,7 +32,6 @@
import {PageErrorEvent} from '../../../types/events';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {GrListView} from '../../shared/gr-list-view/gr-list-view';
-import {SHOWN_ITEMS_COUNT} from '../../../constants/constants';
import {fixture, html, assert} from '@open-wc/testing';
import {RepoDetailView} from '../../../models/views/repo';
@@ -2391,7 +2390,9 @@
});
test('items', () => {
- assert.equal(element.items!.slice(0, SHOWN_ITEMS_COUNT)!.length, 25);
+ const table = queryAndAssert(element, 'table');
+ const rows = table.querySelectorAll('tr.table');
+ assert.equal(rows.length, element.itemsPerPage);
});
});
@@ -2411,7 +2412,9 @@
});
test('items', () => {
- assert.equal(element.items!.slice(0, SHOWN_ITEMS_COUNT)!.length, 25);
+ const table = queryAndAssert(element, 'table');
+ const rows = table.querySelectorAll('tr.table');
+ assert.equal(rows.length, element.itemsPerPage);
});
});
@@ -2434,6 +2437,18 @@
});
suite('create new', () => {
+ setup(async () => {
+ stubRestApi('getRepoBranches').resolves(createBranchesList(3));
+
+ element.params = {
+ view: GerritView.REPO,
+ repo: 'test' as RepoName,
+ detail: RepoDetailView.BRANCHES,
+ };
+ await element.paramsChanged();
+ await element.updateComplete;
+ });
+
test('handleCreateClicked called when create-click fired', () => {
const handleCreateClickedStub = sinon.stub(
element,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
index c6c6efd..055cb30 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
@@ -6,23 +6,23 @@
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-list-view/gr-list-view';
import '../gr-create-repo-dialog/gr-create-repo-dialog';
-import {
- RepoName,
- ProjectInfoWithName,
- WebLinkInfo,
-} from '../../../types/common';
+import {ProjectInfoWithName, WebLinkInfo} from '../../../types/common';
import {GrCreateRepoDialog} from '../gr-create-repo-dialog/gr-create-repo-dialog';
-import {RepoState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {RepoState} from '../../../constants/constants';
import {fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
-import {encodeURL, getBaseUrl} from '../../../utils/url-util';
import {tableStyles} from '../../../styles/gr-table-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
-import {AdminViewState} from '../../../models/views/admin';
+import {
+ AdminChildView,
+ AdminViewState,
+ createAdminUrl,
+} from '../../../models/views/admin';
import {createSearchUrl} from '../../../models/views/search';
import {modalStyles} from '../../../styles/gr-modal-styles';
+import {createRepoUrl} from '../../../models/views/repo';
declare global {
interface HTMLElementTagNameMap {
@@ -32,8 +32,6 @@
@customElement('gr-repo-list')
export class GrRepoList extends LitElement {
- readonly path = '/admin/repos';
-
@query('#createModal') private createModal?: HTMLDialogElement;
@query('#createNewModal') private createNewModal?: GrCreateRepoDialog;
@@ -41,23 +39,18 @@
@property({type: Object})
params?: AdminViewState;
- // private but used in test
@state() offset = 0;
- @state() private newRepoName = false;
+ @state() newRepoName = false;
- @state() private createNewCapability = false;
+ @state() createNewCapability = false;
- // private but used in test
@state() repos: ProjectInfoWithName[] = [];
- // private but used in test
@state() reposPerPage = 25;
- // private but used in test
@state() loading = true;
- // private but used in test
@state() filter = '';
private readonly restApiService = getAppContext().restApiService;
@@ -103,7 +96,7 @@
.items=${this.repos}
.loading=${this.loading}
.offset=${this.offset}
- .path=${this.path}
+ .path=${createAdminUrl({adminView: AdminChildView.REPOS})}
@create-clicked=${() => this.handleCreateClicked()}
>
<table id="list" class="genericList">
@@ -149,7 +142,7 @@
}
private renderRepoList() {
- const shownRepos = this.repos.slice(0, SHOWN_ITEMS_COUNT);
+ const shownRepos = this.repos.slice(0, this.reposPerPage);
return shownRepos.map(item => this.renderRepo(item));
}
@@ -157,11 +150,11 @@
return html`
<tr class="table">
<td class="name">
- <a href=${this.computeRepoUrl(item.name)}>${item.name}</a>
+ <a href=${createRepoUrl({repo: item.name})}>${item.name}</a>
</td>
<td class="repositoryBrowser">${this.renderWebLinks(item)}</td>
<td class="changesLink">
- <a href=${this.computeChangesLink(item.name)}>view all</a>
+ <a href=${createSearchUrl({repo: item.name})}>view all</a>
</td>
<td class="readOnly">
${item.state === RepoState.READ_ONLY ? 'Y' : ''}
@@ -210,14 +203,6 @@
}
}
- private computeRepoUrl(name: string) {
- return `${getBaseUrl()}${this.path}/${encodeURL(name, true)}`;
- }
-
- private computeChangesLink(name: string) {
- return createSearchUrl({repo: name as RepoName});
- }
-
private async getCreateRepoCapability() {
const account = await this.restApiService.getAccount();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
index 5b65942..65f5a8c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
@@ -17,7 +17,7 @@
ProjectInfoWithName,
RepoName,
} from '../../../types/common';
-import {RepoState, SHOWN_ITEMS_COUNT} from '../../../constants/constants';
+import {RepoState} from '../../../api/rest-api';
import {GerritView} from '../../../services/router/router-model';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {GrListView} from '../../shared/gr-list-view/gr-list-view';
@@ -614,7 +614,9 @@
});
test('shownRepos', () => {
- assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ const table = queryAndAssert(element, 'table');
+ const rows = table.querySelectorAll('tr.table');
+ assert.equal(rows.length, element.reposPerPage);
});
test('maybeOpenCreateModal', () => {
@@ -645,7 +647,9 @@
});
test('shownRepos', () => {
- assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25);
+ const table = queryAndAssert(element, 'table');
+ const rows = table.querySelectorAll('tr.table');
+ assert.equal(rows.length, element.reposPerPage);
});
});
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
index 8d6d89a..3599224 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.ts
@@ -25,7 +25,7 @@
RepoState,
SubmitType,
} from '../../../constants/constants';
-import {hasOwnProperty} from '../../../utils/common-util';
+import {assertIsDefined, hasOwnProperty} from '../../../utils/common-util';
import {firePageError, fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {WebLinkInfo} from '../../../types/diff';
@@ -36,8 +36,9 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {BindValueChangeEvent} from '../../../types/events';
import {deepClone} from '../../../utils/deep-util';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {LitElement, PropertyValues, css, html, nothing} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
+import {when} from 'lit/directives/when.js';
import {subscribe} from '../../lit/subscription-controller';
import {createSearchUrl} from '../../../models/views/search';
import {userModelToken} from '../../../models/user/user-model';
@@ -150,16 +151,6 @@
color: var(--deemphasized-text-color);
content: ' *';
}
- .loading,
- .hide {
- display: none;
- }
- #loading.loading {
- display: block;
- }
- #loading:not(.loading) {
- display: none;
- }
#options .repositorySettings {
display: none;
}
@@ -187,49 +178,48 @@
>
</div>
</div>
- <div id="loading" class=${this.loading ? 'loading' : ''}>
- Loading...
- </div>
- <div id="loadedContent" class=${this.loading ? 'loading' : ''}>
- ${this.renderDownloadCommands()}
- <h2
- id="configurations"
- class="heading-2 ${configChanged ? 'edited' : ''}"
- >
- Configurations
- </h2>
- <div id="form">
- <fieldset>
- ${this.renderDescription()} ${this.renderRepoOptions()}
- ${this.renderPluginConfig()}
- <gr-button
- ?disabled=${this.readOnly || !configChanged}
- @click=${this.handleSaveRepoConfig}
- >Save changes</gr-button
- >
- </fieldset>
- <gr-endpoint-decorator name="repo-config">
- <gr-endpoint-param
- name="repoName"
- .value=${this.repo}
- ></gr-endpoint-param>
- <gr-endpoint-param
- name="readOnly"
- .value=${this.readOnly}
- ></gr-endpoint-param>
- </gr-endpoint-decorator>
- </div>
- </div>
+ ${when(
+ this.loading || !this.repoConfig,
+ () => html`<div id="loading">Loading...</div>`,
+ () => html`<div id="loadedContent">
+ ${this.renderDownloadCommands()}
+ <h2
+ id="configurations"
+ class="heading-2 ${configChanged ? 'edited' : ''}"
+ >
+ Configurations
+ </h2>
+ <div id="form">
+ <fieldset>
+ ${this.renderDescription()} ${this.renderRepoOptions()}
+ ${this.renderPluginConfig()}
+ <gr-button
+ ?disabled=${this.readOnly || !configChanged}
+ @click=${this.handleSaveRepoConfig}
+ >Save changes</gr-button
+ >
+ </fieldset>
+ <gr-endpoint-decorator name="repo-config">
+ <gr-endpoint-param
+ name="repoName"
+ .value=${this.repo}
+ ></gr-endpoint-param>
+ <gr-endpoint-param
+ name="readOnly"
+ .value=${this.readOnly}
+ ></gr-endpoint-param>
+ </gr-endpoint-decorator>
+ </div>
+ </div>`
+ )}
</div>
`;
}
private renderDownloadCommands() {
+ if (!this.schemes.length) return nothing;
return html`
- <div
- id="downloadContent"
- class=${!this.schemes || !this.schemes.length ? 'hide' : ''}
- >
+ <div id="downloadContent">
<h2 id="download" class="heading-2">Download</h2>
<fieldset>
<gr-download-commands
@@ -252,6 +242,7 @@
}
private renderDescription() {
+ assertIsDefined(this.repoConfig, 'repoConfig');
return html`
<h3 id="Description" class="heading-3">Description</h3>
<fieldset>
@@ -263,7 +254,7 @@
rows="4"
monospace
?disabled=${this.readOnly}
- .text=${this.repoConfig?.description ?? ''}
+ .text=${this.repoConfig.description ?? ''}
@text-changed=${this.handleDescriptionTextChanged}
></gr-textarea>
</fieldset>
@@ -725,8 +716,9 @@
private renderPluginConfig() {
const pluginData = this.computePluginData();
+ if (!pluginData.length) return nothing;
return html` <div
- class="pluginConfig ${!pluginData || !pluginData.length ? 'hide' : ''}"
+ class="pluginConfig"
@plugin-config-changed=${this.handlePluginConfigChanged}
>
<h3 class="heading-3">Plugins</h3>
@@ -762,6 +754,12 @@
// private but used in test
async loadRepo() {
if (!this.repo) return Promise.resolve();
+ this.repoConfig = undefined;
+ this.originalConfig = undefined;
+ this.loading = true;
+ this.weblinks = [];
+ this.schemesObj = undefined;
+ this.readOnly = true;
const promises = [];
@@ -1121,6 +1119,7 @@
private handleDescriptionTextChanged(e: BindValueChangeEvent) {
if (!this.repoConfig || this.loading) return;
+ if (this.repoConfig.description === e.detail.value) return;
this.repoConfig = {
...this.repoConfig,
description: e.detail.value,
@@ -1130,6 +1129,7 @@
private handleStateSelectBindValueChanged(e: BindValueChangeEvent) {
if (!this.repoConfig || this.loading) return;
+ if (this.repoConfig.state === e.detail.value) return;
this.repoConfig = {
...this.repoConfig,
state: e.detail.value as RepoState,
@@ -1139,6 +1139,7 @@
private handleSubmitTypeSelectBindValueChanged(e: BindValueChangeEvent) {
if (!this.repoConfig || this.loading) return;
+ if (this.repoConfig.submit_type === e.detail.value) return;
this.repoConfig = {
...this.repoConfig,
submit_type: e.detail.value as SubmitType,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
index c013c9e..4deb99a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.ts
@@ -157,14 +157,17 @@
element = await fixture(html`<gr-repo></gr-repo>`);
});
- test('render', () => {
+ test('render', async () => {
+ element.repo = REPO as RepoName;
+ await element.loadRepo();
+ await element.updateComplete;
// prettier and shadowDom assert do not agree about span.title wrapping
assert.shadowDom.equal(
element,
/* prettier-ignore */ /* HTML */ `
<div class="gr-form-styles main read-only">
<div class="info">
- <h1 class="heading-1" id="Title"></h1>
+ <h1 class="heading-1" id="Title">test-repo</h1>
<hr />
<div>
<a href="">
@@ -178,7 +181,7 @@
Browse
</gr-button>
</a>
- <a href="">
+ <a href="/q/project:test-repo">
<gr-button
aria-disabled="false"
link=""
@@ -190,15 +193,7 @@
</a>
</div>
</div>
- <div class="loading" id="loading">Loading...</div>
- <div class="loading" id="loadedContent">
- <div class="hide" id="downloadContent">
- <h2 class="heading-2" id="download">Download</h2>
- <fieldset>
- <gr-download-commands id="downloadCommands">
- </gr-download-commands>
- </fieldset>
- </div>
+ <div id="loadedContent">
<h2 class="heading-2" id="configurations">Configurations</h2>
<div id="form">
<fieldset>
@@ -266,7 +261,7 @@
</span>
</section>
<section
- class="repositorySettings"
+ class="repositorySettings showConfig"
id="enableSignedPushSettings"
>
<span class="title"> Enable signed push </span>
@@ -277,7 +272,7 @@
</span>
</section>
<section
- class="repositorySettings"
+ class="repositorySettings showConfig"
id="requireSignedPushSettings"
>
<span class="title"> Require signed push </span>
@@ -379,9 +374,6 @@
</span>
</section>
</fieldset>
- <div class="hide pluginConfig">
- <h3 class="heading-3">Plugins</h3>
- </div>
<gr-button
aria-disabled="true"
disabled=""
@@ -398,7 +390,51 @@
</div>
</div>
</div>
- `
+ `,
+ {ignoreTags: ['option']}
+ );
+ });
+
+ test('render loading', async () => {
+ element.repo = REPO as RepoName;
+ element.loading = true;
+ await element.updateComplete;
+ // prettier and shadowDom assert do not agree about span.title wrapping
+ assert.shadowDom.equal(
+ element,
+ /* prettier-ignore */ /* HTML */ `
+ <div class="gr-form-styles main read-only">
+ <div class="info">
+ <h1 class="heading-1" id="Title">test-repo</h1>
+ <hr />
+ <div>
+ <a href="">
+ <gr-button
+ aria-disabled="true"
+ disabled=""
+ link=""
+ role="button"
+ tabindex="-1"
+ >
+ Browse
+ </gr-button>
+ </a>
+ <a href="/q/project:test-repo">
+ <gr-button
+ aria-disabled="false"
+ link=""
+ role="button"
+ tabindex="0"
+ >
+ View Changes
+ </gr-button>
+ </a>
+ </div>
+ </div>
+ <div id="loading">Loading...</div>
+ </div>
+ `,
+ {ignoreTags: ['option']}
);
});
@@ -451,55 +487,22 @@
assert.isTrue(requestUpdateStub.called);
});
- test('loading displays before repo config is loaded', () => {
- assert.isTrue(
- queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
- 'loading'
- )
- );
- assert.isFalse(
- getComputedStyle(queryAndAssert<HTMLDivElement>(element, '#loading'))
- .display === 'none'
- );
- assert.isTrue(
- queryAndAssert<HTMLDivElement>(
- element,
- '#loadedContent'
- ).classList.contains('loading')
- );
- assert.isTrue(
- getComputedStyle(
- queryAndAssert<HTMLDivElement>(element, '#loadedContent')
- ).display === 'none'
- );
- });
-
- test('download commands visibility', async () => {
- element.loading = false;
- await element.updateComplete;
- assert.isTrue(
- queryAndAssert<HTMLDivElement>(
- element,
- '#downloadContent'
- ).classList.contains('hide')
- );
- assert.isTrue(
- getComputedStyle(
- queryAndAssert<HTMLDivElement>(element, '#downloadContent')
- ).display === 'none'
- );
+ test('render download commands', async () => {
+ element.repo = REPO as RepoName;
+ await element.loadRepo();
element.schemesObj = SCHEMES;
await element.updateComplete;
- assert.isFalse(
- queryAndAssert<HTMLDivElement>(
- element,
- '#downloadContent'
- ).classList.contains('hide')
- );
- assert.isFalse(
- getComputedStyle(
- queryAndAssert<HTMLDivElement>(element, '#downloadContent')
- ).display === 'none'
+ const content = queryAndAssert<HTMLDivElement>(element, '#downloadContent');
+ assert.dom.equal(
+ content,
+ /* HTML */ `
+ <div id="downloadContent">
+ <h2 class="heading-2" id="download">Download</h2>
+ <fieldset>
+ <gr-download-commands id="downloadCommands"></gr-download-commands>
+ </fieldset>
+ </div>
+ `
);
});
@@ -715,9 +718,9 @@
Promise.resolve(new Response())
);
- const button = queryAll<GrButton>(element, 'gr-button')[2];
-
await element.loadRepo();
+
+ const button = queryAll<GrButton>(element, 'gr-button')[2];
assert.isTrue(button.hasAttribute('disabled'));
assert.isFalse(
queryAndAssert<HTMLHeadingElement>(
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
index 71977d1..654ed91 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-bulk-vote-flow/gr-change-list-bulk-vote-flow_test.ts
@@ -286,7 +286,7 @@
</div>
</div>
</gr-dialog>
- </gr-overlay> `
+ </dialog> `
);
});
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index f206a9a..342b876 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -26,6 +26,7 @@
ServerInfo,
AccountInfo,
Timestamp,
+ NumericChangeId,
} from '../../../types/common';
import {hasOwnProperty, assertIsDefined} from '../../../utils/common-util';
import {changeListStyles} from '../../../styles/gr-change-list-styles';
@@ -130,8 +131,7 @@
this,
() => this.getBulkActionsModel().selectedChangeNums$,
selectedChangeNums => {
- if (!this.change) return;
- this.checked = selectedChangeNums.includes(this.change._number);
+ this.updateCheckedState(selectedChangeNums);
}
);
}
@@ -159,6 +159,20 @@
if (this.selected && changedProperties.has('selected')) {
this.focus();
}
+
+ if (changedProperties.has('change')) {
+ this.updateCheckedState(
+ this.getBulkActionsModel().getState().selectedChangeNums
+ );
+ }
+ }
+
+ private updateCheckedState(selectedChangeNums: NumericChangeId[]) {
+ if (!this.change) {
+ this.checked = false;
+ return;
+ }
+ this.checked = selectedChangeNums.includes(this.change._number);
}
static override get styles() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index e8f7fc3..c7cb5b8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -153,10 +153,6 @@
element.change = {...createChange(), _number: 1 as NumericChangeId};
bulkActionsModel.sync([element.change]);
bulkActionsModel.addSelectedChangeNum(element.change._number);
- await waitUntilObserved(
- bulkActionsModel.selectedChangeNums$,
- s => s.length === 1
- );
await element.updateComplete;
const checkbox = queryAndAssert<HTMLInputElement>(
@@ -166,10 +162,31 @@
assert.isTrue(checkbox.checked);
bulkActionsModel.removeSelectedChangeNum(element.change._number);
- await waitUntilObserved(
- bulkActionsModel.selectedChangeNums$,
- s => s.length === 0
+ await element.updateComplete;
+
+ assert.isFalse(checkbox.checked);
+ });
+
+ test('checkbox state updates with change id update', async () => {
+ element.requestUpdate();
+ await element.updateComplete;
+
+ const changes = [
+ {...createChange(), _number: 1 as NumericChangeId},
+ {...createChange(), _number: 2 as NumericChangeId},
+ ];
+ element.change = changes[0];
+ bulkActionsModel.sync(changes);
+ bulkActionsModel.addSelectedChangeNum(element.change._number);
+ await element.updateComplete;
+
+ const checkbox = queryAndAssert<HTMLInputElement>(
+ element,
+ '.selection > .selectionLabel > input'
);
+ assert.isTrue(checkbox.checked);
+
+ element.change = changes[1];
await element.updateComplete;
assert.isFalse(checkbox.checked);
@@ -341,12 +358,14 @@
});
test('renders', async () => {
+ const change = createChange();
+ bulkActionsModel.sync([change]);
+ bulkActionsModel.addSelectedChangeNum(change._number);
element.showStar = true;
element.showNumber = true;
element.account = createAccountWithId(1);
element.config = createServerInfo();
- element.change = createChange();
- element.checked = true;
+ element.change = change;
await element.updateComplete;
assert.isTrue(element.hasAttribute('checked'));
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
index 1789fcb..ab116f1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-reviewer-flow/gr-change-list-reviewer-flow.ts
@@ -268,7 +268,7 @@
<dialog
tabindex="-1"
id=${id}
- @iron-overlay-canceled=${() => this.cancelPendingGroup(reviewerState)}
+ @close=${() => this.cancelPendingGroup(reviewerState)}
>
<div class="confirmation-text">
Group
@@ -466,11 +466,11 @@
}
private cancelPendingGroup(reviewerState: ReviewerState) {
- const overlay =
+ const modal =
reviewerState === ReviewerState.CC
? this.ccConfirmModal
: this.reviewerConfirmModal;
- overlay?.close();
+ modal?.close();
this.groupPendingConfirmationByReviewerState.set(reviewerState, null);
this.requestUpdate();
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index 7abfce9..faaee0b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -282,12 +282,14 @@
// private but used in test
handleNextPage() {
if (!this.nextArrow || !this.changesPerPage) return;
+ // TODO: Use navigation service instead of `page.show()` directly.
page.show(this.computeNavLink(1));
}
// private but used in test
handlePreviousPage() {
if (!this.prevArrow || !this.changesPerPage) return;
+ // TODO: Use navigation service instead of `page.show()` directly.
page.show(this.computeNavLink(-1));
}
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index 6e0e3a9..d013654 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -6,7 +6,6 @@
import '../gr-change-list/gr-change-list';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-dialog/gr-dialog';
-import '../../shared/gr-overlay/gr-overlay';
import '../gr-create-commands-dialog/gr-create-commands-dialog';
import '../gr-create-change-help/gr-create-change-help';
import '../gr-create-destination-dialog/gr-create-destination-dialog';
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 a78ba1c..e47b450 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
@@ -8,7 +8,6 @@
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-dropdown/gr-dropdown';
import '../../shared/gr-icon/gr-icon';
-import '../../shared/gr-overlay/gr-overlay';
import '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog';
import '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog';
import '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog';
@@ -38,6 +37,7 @@
ActionInfo,
ActionNameToActionInfoMap,
BranchName,
+ ChangeActionDialog,
ChangeInfo,
ChangeViewChangeInfo,
CherryPickInput,
@@ -317,11 +317,6 @@
priority: ActionPriority;
}
-interface ChangeActionDialog extends HTMLElement {
- resetFocus?(): void;
- init?(): void;
-}
-
@customElement('gr-change-actions')
export class GrChangeActions
extends LitElement
@@ -450,6 +445,8 @@
// private but used in test
@state() actionLoadingMessage = '';
+ @state() private inProgressActionKeys = new Set<string>();
+
// _computeAllActions always returns an array
// private but used in test
@state() allActionValues: UIActionInfo[] = [];
@@ -676,6 +673,9 @@
.changeNumber=${this.change?._number}
@confirm=${this.handleRebaseConfirm}
@cancel=${this.handleConfirmDialogCancel}
+ .disableActions=${this.inProgressActionKeys.has(
+ RevisionActions.REBASE
+ )}
.branch=${this.change?.branch}
.hasParent=${this.hasParent}
.rebaseOnCurrent=${this.revisionRebaseAction
@@ -1577,10 +1577,11 @@
base: e.detail.base,
allow_conflicts: e.detail.allowConflicts,
};
+ const rebaseChain = !!e.detail.rebaseChain;
this.fireAction(
- '/rebase',
+ rebaseChain ? '/rebase:chain' : '/rebase',
assertUIActionInfo(this.revisionActions.rebase),
- true,
+ rebaseChain ? false : true,
payload,
{allow_conflicts: payload.allow_conflicts}
);
@@ -1746,7 +1747,9 @@
}
// private but used in test
- setLoadingOnButtonWithKey(type: string, key: string) {
+ setLoadingOnButtonWithKey(action: UIActionInfo) {
+ const key = action.__key;
+ this.inProgressActionKeys.add(key);
this.actionLoadingMessage = this.computeLoadingLabel(key);
let buttonKey = key;
// TODO(dhruvsri): clean this up later
@@ -1757,12 +1760,14 @@
}
// If the action appears in the overflow menu.
- if (this.getActionOverflowIndex(type, buttonKey) !== -1) {
+ if (this.getActionOverflowIndex(action.__type, buttonKey) !== -1) {
this.disabledMenuActions.push(buttonKey === '/' ? 'delete' : buttonKey);
this.requestUpdate('disabledMenuActions');
return () => {
+ this.inProgressActionKeys.delete(key);
this.actionLoadingMessage = '';
this.disabledMenuActions = [];
+ this.requestUpdate();
};
}
@@ -1776,9 +1781,11 @@
buttonEl.setAttribute('loading', 'true');
buttonEl.disabled = true;
return () => {
+ this.inProgressActionKeys.delete(action.__key);
this.actionLoadingMessage = '';
buttonEl.removeAttribute('loading');
buttonEl.disabled = false;
+ this.requestUpdate();
};
}
@@ -1790,10 +1797,7 @@
payload?: RequestPayload,
toReport?: Object
) {
- const cleanupFn = this.setLoadingOnButtonWithKey(
- action.__type,
- action.__key
- );
+ const cleanupFn = this.setLoadingOnButtonWithKey(action);
this.reporting.reportInteraction(Interaction.CHANGE_ACTION_FIRED, {
endpoint,
toReport,
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index bdcdc31..4602eac 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -625,7 +625,9 @@
};
assert.isTrue(fetchChangesStub.called);
element.handleRebaseConfirm(
- new CustomEvent('', {detail: {base: '1234', allowConflicts: false}})
+ new CustomEvent('', {
+ detail: {base: '1234', allowConflicts: false, rebaseChain: false},
+ })
);
assert.deepEqual(fireActionStub.lastCall.args, [
'/rebase',
@@ -1269,18 +1271,28 @@
await keyTapped;
});
- test('setLoadingOnButtonWithKey top-level', () => {
+ test('setLoadingOnButtonWithKey top-level', async () => {
const key = 'rebase';
- const type = 'revision';
- const cleanup = element.setLoadingOnButtonWithKey(type, key);
+ const type = ActionType.REVISION;
+ const cleanup = element.setLoadingOnButtonWithKey({
+ __type: type,
+ __key: key,
+ label: 'label',
+ });
assert.equal(element.actionLoadingMessage, 'Rebasing...');
const button = queryAndAssert<GrButton>(
element,
'[data-action-key="' + key + '"]'
);
+ const dialog = queryAndAssert<GrConfirmRebaseDialog>(
+ element,
+ 'gr-confirm-rebase-dialog'
+ );
assert.isTrue(button.hasAttribute('loading'));
assert.isTrue(button.disabled);
+ await dialog.updateComplete;
+ assert.isTrue(dialog.disableActions);
assert.isOk(cleanup);
assert.isFunction(cleanup);
@@ -1289,12 +1301,18 @@
assert.isFalse(button.hasAttribute('loading'));
assert.isFalse(button.disabled);
assert.isNotOk(element.actionLoadingMessage);
+ await dialog.updateComplete;
+ assert.isFalse(dialog.disableActions);
});
test('setLoadingOnButtonWithKey overflow menu', () => {
const key = 'cherrypick';
- const type = 'revision';
- const cleanup = element.setLoadingOnButtonWithKey(type, key);
+ const type = ActionType.REVISION;
+ const cleanup = element.setLoadingOnButtonWithKey({
+ __type: type,
+ __key: key,
+ label: 'label',
+ });
assert.equal(element.actionLoadingMessage, 'Cherry-picking...');
assert.include(element.disabledMenuActions, 'cherrypick');
assert.isFunction(cleanup);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index a06f8c9..b9a04bd 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -9,7 +9,6 @@
import '../../../styles/gr-change-view-integration-shared-styles';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
-import '../../plugins/gr-external-style/gr-external-style';
import '../../shared/gr-account-chip/gr-account-chip';
import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../shared/gr-editable-label/gr-editable-label';
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 84bdffb..b726292 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -55,7 +55,6 @@
import {SummaryChipStyles} from './gr-summary-chip';
import {when} from 'lit/directives/when.js';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {combineLatest} from 'rxjs';
import {userModelToken} from '../../../models/user/user-model';
@@ -120,8 +119,6 @@
private readonly reporting = getAppContext().reportingService;
- private readonly flagsService = getAppContext().flagsService;
-
constructor() {
super();
subscribe(
@@ -174,24 +171,22 @@
() => this.getUserModel().account$,
x => (this.selfAccount = x)
);
- if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
- subscribe(
- this,
- () =>
- combineLatest([
- this.getUserModel().account$,
- this.getCommentsModel().threads$,
- ]),
- ([selfAccount, threads]) => {
- if (!selfAccount || !selfAccount.email) return;
- const unresolvedThreadsMentioningSelf = getMentionedThreads(
- threads,
- selfAccount
- ).filter(isUnresolved);
- this.mentionCount = unresolvedThreadsMentioningSelf.length;
- }
- );
- }
+ subscribe(
+ this,
+ () =>
+ combineLatest([
+ this.getUserModel().account$,
+ this.getCommentsModel().threads$,
+ ]),
+ ([selfAccount, threads]) => {
+ if (!selfAccount || !selfAccount.email) return;
+ const unresolvedThreadsMentioningSelf = getMentionedThreads(
+ threads,
+ selfAccount
+ ).filter(isUnresolved);
+ this.mentionCount = unresolvedThreadsMentioningSelf.length;
+ }
+ );
}
static override get styles() {
@@ -575,8 +570,6 @@
}
private renderMentionChip() {
- if (!this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS))
- return nothing;
if (!this.mentionCount) return nothing;
return html` <gr-summary-chip
class="mentionSummary"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 91b82dc..d4627e2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -16,7 +16,6 @@
import '../../shared/gr-change-status/gr-change-status';
import '../../shared/gr-editable-content/gr-editable-content';
import '../../shared/gr-formatted-text/gr-formatted-text';
-import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../gr-change-actions/gr-change-actions';
import '../gr-change-summary/gr-change-summary';
@@ -35,11 +34,7 @@
import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
import {GrEditConstants} from '../../edit/gr-edit-constants';
import {pluralize} from '../../../utils/string-util';
-import {
- querySelectorAll,
- whenVisible,
- windowLocationReload,
-} from '../../../utils/dom-util';
+import {whenVisible, windowLocationReload} from '../../../utils/dom-util';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
import {
@@ -70,7 +65,6 @@
import {GrApplyFixDialog} from '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog';
import {GrFileListHeader} from '../gr-file-list-header/gr-file-list-header';
import {GrEditableContent} from '../../shared/gr-editable-content/gr-editable-content';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {GrRelatedChangesList} from '../gr-related-changes-list/gr-related-changes-list';
import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
import {GrChangeActions} from '../gr-change-actions/gr-change-actions';
@@ -141,16 +135,12 @@
fireTitleChange,
} from '../../../utils/event-util';
import {
- GerritView,
- routerModelToken,
-} from '../../../services/router/router-model';
-import {
debounce,
DelayedTask,
throttleWrap,
until,
} from '../../../utils/async-util';
-import {Interaction, Timing, Execution} from '../../../constants/reporting';
+import {Interaction, Timing} from '../../../constants/reporting';
import {ChangeStates} from '../../shared/gr-change-status/gr-change-status';
import {getRevertCreatedChangeIds} from '../../../utils/message-util';
import {
@@ -182,12 +172,13 @@
import {getBaseUrl, prependOrigin} from '../../../utils/url-util';
import {CopyLink, GrCopyLinks} from '../gr-copy-links/gr-copy-links';
import {
+ ChangeChildView,
changeViewModelToken,
ChangeViewState,
createChangeUrl,
+ createEditUrl,
} from '../../../models/views/change';
import {rootUrl} from '../../../utils/url-util';
-import {createEditUrl} from '../../../models/views/edit';
import {userModelToken} from '../../../models/user/user-model';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {modalStyles} from '../../../styles/gr-modal-styles';
@@ -545,8 +536,6 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly getRouterModel = resolve(this, routerModelToken);
-
private readonly getCommentsModel = resolve(this, commentsModelToken);
private readonly getConfigModel = resolve(this, configModelToken);
@@ -579,7 +568,7 @@
/** Simply reflects the router-model value. */
// visible for testing
- routerPatchNum?: PatchSetNum;
+ viewModelPatchNum?: PatchSetNum;
private readonly shortcutsController = new ShortcutController(this);
@@ -706,16 +695,16 @@
);
subscribe(
this,
- () => this.getRouterModel().routerView$,
- view => {
- this.isViewCurrent = view === GerritView.CHANGE;
+ () => this.getViewModel().childView$,
+ childView => {
+ this.isViewCurrent = childView === ChangeChildView.OVERVIEW;
}
);
subscribe(
this,
- () => this.getRouterModel().routerPatchNum$,
+ () => this.getViewModel().patchNum$,
patchNum => {
- this.routerPatchNum = patchNum;
+ this.viewModelPatchNum = patchNum;
}
);
subscribe(
@@ -889,7 +878,6 @@
border-bottom: 1px solid var(--border-color);
display: flex;
padding: var(--spacing-s) var(--spacing-l);
- z-index: 99; /* Less than gr-overlay's backdrop */
}
.header.editMode {
background-color: var(--edit-mode-background-color);
@@ -1268,7 +1256,14 @@
flatten
down-arrow
class="showCopyLinkDialogButton"
- @click=${() => this.copyLinksDropdown?.toggleDropdown()}
+ @click=${(e: MouseEvent) => {
+ // We don't want to handle clicks on the star or the <a> link.
+ // Calling `stopPropagation()` from the click handler of <a> is not an
+ // option, because then the click does not reach the top-level page.js
+ // click handler and would result is a full page reload.
+ if ((e.target as HTMLElement)?.nodeName !== 'GR-BUTTON') return;
+ this.copyLinksDropdown?.toggleDropdown();
+ }}
><gr-change-star
id="changeStar"
.change=${this.change}
@@ -1280,7 +1275,6 @@
class="changeNumber"
aria-label=${`Change ${this.change?._number}`}
href=${ifDefined(this.computeChangeUrl(true))}
- @click=${(e: MouseEvent) => e.stopPropagation()}
>${this.change?._number}</a
>
</gr-button>
@@ -1536,7 +1530,6 @@
.allPatchSets=${this.allPatchSets}
.change=${this.change}
.changeNum=${this.changeNum}
- .revisionInfo=${this.getRevisionInfo()}
.commitInfo=${this.commitInfo}
.changeUrl=${this.computeChangeUrl()}
.editMode=${this.getEditMode()}
@@ -2086,13 +2079,7 @@
// Private but used in tests.
viewStateChanged() {
- if (this.viewState === undefined) {
- this.initialLoadComplete = false;
- querySelectorAll(this, 'gr-overlay').forEach(overlay =>
- (overlay as GrOverlay).close()
- );
- return;
- }
+ if (!this.viewState) return;
if (this.isChangeObsolete()) {
// Tell the app element that we are not going to handle the new change
@@ -2101,13 +2088,6 @@
return;
}
- if (this.viewState.changeNum && this.viewState.repo) {
- this.restApiService.setInProjectLookup(
- this.viewState.changeNum,
- this.viewState.repo
- );
- }
-
if (this.viewState.basePatchNum === undefined)
this.viewState.basePatchNum = PARENT;
@@ -2293,7 +2273,7 @@
private updateTitle(change?: ChangeInfo | ParsedChangeInfo) {
if (!change) return;
- const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')';
+ const title = `${change.subject} (${change._number})`;
fireTitleChange(this, title);
}
@@ -2352,13 +2332,10 @@
this.prefs &&
this.prefs.default_base_for_merges === DefaultBase.FIRST_PARENT;
- // TODO: I think checking `!patchRange.patchNum` here is a bug and means
- // that the feature is actually broken at the moment. Looking at the
- // `changeChanged` method, `patchRange.patchNum` is set before
- // `getBasePatchNum` is called, so it is unlikely that this method will
- // ever return -1.
+ // Verified via reportExecution that -1 is returned(1-5 times per day)
+ // changeChanged does set this.patchRange?.patchNum so it's still unclear
+ // how it is undefined.
if (isMerge && preferFirst && !this.patchRange?.patchNum) {
- this.reporting.reportExecution(Execution.PREFER_MERGE_FIRST_PARENT);
return -1 as BasePatchSetNum;
}
return PARENT;
@@ -2460,6 +2437,7 @@
// Private but used in tests.
handleDiffBaseAgainstLeft() {
+ if (this.viewState?.childView !== ChangeChildView.OVERVIEW) return;
assertIsDefined(this.change, 'change');
assertIsDefined(this.patchRange, 'patchRange');
@@ -2662,7 +2640,7 @@
// is under change-model control. `patchRange.patchNum` should eventually
// also be model managed, so we can reconcile these two code snippets into
// one location.
- if (!this.routerPatchNum && latestPsNum === editParentRev._number) {
+ if (!this.viewModelPatchNum && latestPsNum === editParentRev._number) {
this.patchRange = {...this.patchRange, patchNum: EDIT};
// The file list is not reactive (yet) with regards to patch range
// changes, so we have to actively trigger it.
@@ -3155,8 +3133,8 @@
createEditUrl({
changeNum: this.change._number,
repo: this.change.project,
- path,
patchNum: this.patchRange.patchNum,
+ editView: {path},
})
);
break;
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 9d89192..9b78e64 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
@@ -99,7 +99,7 @@
import {Modifier} from '../../../utils/dom-util';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrCopyLinks} from '../gr-copy-links/gr-copy-links';
-import {ChangeViewState} from '../../../models/views/change';
+import {ChangeChildView, ChangeViewState} from '../../../models/views/change';
import {rootUrl} from '../../../utils/url-util';
import {testResolver} from '../../../test/common-test-setup';
import {UserModel, userModelToken} from '../../../models/user/user-model';
@@ -369,6 +369,7 @@
);
element.viewState = {
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
changeNum: TEST_NUMERIC_CHANGE_ID,
repo: 'gerrit' as RepoName,
};
@@ -1986,7 +1987,7 @@
// When edit is set, but patchNum as well, then keep patchNum.
element.patchRange.patchNum = 5 as RevisionPatchSetNum;
- element.routerPatchNum = 5 as RevisionPatchSetNum;
+ element.viewModelPatchNum = 5 as RevisionPatchSetNum;
element.processEdit(change);
assert.equal(element.patchRange.patchNum, 5 as RevisionPatchSetNum);
});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
index ab15ae6..85746df 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.ts
@@ -13,6 +13,7 @@
import {assertIsDefined} from '../../../utils/common-util';
import {BindValueChangeEvent} from '../../../types/events';
import {ShortcutController} from '../../lit/shortcut-controller';
+import {ChangeActionDialog} from '../../../types/common';
declare global {
interface HTMLElementTagNameMap {
@@ -21,7 +22,10 @@
}
@customElement('gr-confirm-abandon-dialog')
-export class GrConfirmAbandonDialog extends LitElement {
+export class GrConfirmAbandonDialog
+ extends LitElement
+ implements ChangeActionDialog
+{
/**
* Fired when the confirm button is pressed.
*
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
index 15f3e21..02156df 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.ts
@@ -6,10 +6,14 @@
import {css, html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
+import {ChangeActionDialog} from '../../../types/common';
import '../../shared/gr-dialog/gr-dialog';
@customElement('gr-confirm-cherrypick-conflict-dialog')
-export class GrConfirmCherrypickConflictDialog extends LitElement {
+export class GrConfirmCherrypickConflictDialog
+ extends LitElement
+ implements ChangeActionDialog
+{
/**
* Fired when the confirm button is pressed.
*
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
index 5d7e55c..5f3b824 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.ts
@@ -17,6 +17,7 @@
CommitId,
ChangeInfoId,
TopicName,
+ ChangeActionDialog,
} from '../../../types/common';
import {customElement, property, query, state} from 'lit/decorators.js';
import {
@@ -60,7 +61,10 @@
}
@customElement('gr-confirm-cherrypick-dialog')
-export class GrConfirmCherrypickDialog extends LitElement {
+export class GrConfirmCherrypickDialog
+ extends LitElement
+ implements ChangeActionDialog
+{
/**
* Fired when the confirm button is pressed.
*
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
index 8f5c8dc..3f84189 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.ts
@@ -6,7 +6,7 @@
import {css, html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
-import {BranchName, RepoName} from '../../../types/common';
+import {BranchName, ChangeActionDialog, RepoName} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-dialog/gr-dialog';
@@ -19,7 +19,10 @@
const SUGGESTIONS_LIMIT = 15;
@customElement('gr-confirm-move-dialog')
-export class GrConfirmMoveDialog extends LitElement {
+export class GrConfirmMoveDialog
+ extends LitElement
+ implements ChangeActionDialog
+{
/**
* Fired when the confirm button is pressed.
*
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 072ec73d..b0dbda5 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -5,7 +5,12 @@
*/
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
-import {NumericChangeId, BranchName} from '../../../types/common';
+import {when} from 'lit/directives/when.js';
+import {
+ NumericChangeId,
+ BranchName,
+ ChangeActionDialog,
+} from '../../../types/common';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-autocomplete/gr-autocomplete';
import {
@@ -17,6 +22,7 @@
import {sharedStyles} from '../../../styles/shared-styles';
import {ValueChangedEvent} from '../../../types/events';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {KnownExperimentId} from '../../../services/flags/flags';
export interface RebaseChange {
name: string;
@@ -26,10 +32,14 @@
export interface ConfirmRebaseEventDetail {
base: string | null;
allowConflicts: boolean;
+ rebaseChain: boolean;
}
@customElement('gr-confirm-rebase-dialog')
-export class GrConfirmRebaseDialog extends LitElement {
+export class GrConfirmRebaseDialog
+ extends LitElement
+ implements ChangeActionDialog
+{
/**
* Fired when the confirm button is pressed.
*
@@ -54,6 +64,9 @@
@property({type: Boolean})
rebaseOnCurrent?: boolean;
+ @property({type: Boolean})
+ disableActions = false;
+
@state()
text = '';
@@ -75,11 +88,16 @@
@query('#rebaseAllowConflicts')
private rebaseAllowConflicts!: HTMLInputElement;
+ @query('#rebaseChain')
+ private rebaseChain?: HTMLInputElement;
+
@query('#parentInput')
parentInput!: GrAutocomplete;
private readonly restApiService = getAppContext().restApiService;
+ private readonly flagsService = getAppContext().flagsService;
+
constructor() {
super();
this.query = input => this.getChangeSuggestions(input);
@@ -130,6 +148,7 @@
<gr-dialog
id="confirmDialog"
confirm-label="Rebase"
+ .disabled=${this.disableActions}
@confirm=${this.handleConfirmTap}
@cancel=${this.handleCancelTap}
>
@@ -210,6 +229,14 @@
>Allow rebase with conflicts</label
>
</div>
+ ${when(
+ this.flagsService.isEnabled(KnownExperimentId.REBASE_CHAIN),
+ () =>
+ html`<div>
+ <input id="rebaseChain" type="checkbox" />
+ <label for="rebaseChain">Rebase all ancestors</label>
+ </div>`
+ )}
</div>
</gr-dialog>
`;
@@ -315,6 +342,7 @@
const detail: ConfirmRebaseEventDetail = {
base: this.getSelectedBase(),
allowConflicts: this.rebaseAllowConflicts.checked,
+ rebaseChain: !!this.rebaseChain?.checked,
};
this.dispatchEvent(new CustomEvent('confirm', {detail}));
this.text = '';
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index eba4bfe..776e923 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -16,6 +16,7 @@
import {createChangeViewChange} from '../../../test/test-data-generators';
import {fixture, html, assert} from '@open-wc/testing';
import {Key} from '../../../utils/dom-util';
+import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
suite('gr-confirm-rebase-dialog tests', () => {
let element: GrConfirmRebaseDialog;
@@ -89,6 +90,19 @@
);
});
+ test('disableActions property disables dialog confirm', async () => {
+ element.disableActions = false;
+ await element.updateComplete;
+
+ const dialog = queryAndAssert<GrDialog>(element, 'gr-dialog');
+ assert.isFalse(dialog.disabled);
+
+ element.disableActions = true;
+ await element.updateComplete;
+
+ assert.isTrue(dialog.disabled);
+ });
+
test('controls with parent and rebase on current available', async () => {
element.rebaseOnCurrent = true;
element.hasParent = true;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
index 6b37284..dd9a5ee 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.ts
@@ -8,7 +8,7 @@
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import {LitElement, html, css, nothing} from 'lit';
import {customElement, state} from 'lit/decorators.js';
-import {ChangeInfo, CommitId} from '../../../types/common';
+import {ChangeActionDialog, ChangeInfo, CommitId} from '../../../types/common';
import {fire, fireAlert} from '../../../utils/event-util';
import {sharedStyles} from '../../../styles/shared-styles';
import {BindValueChangeEvent} from '../../../types/events';
@@ -46,7 +46,10 @@
}
@customElement('gr-confirm-revert-dialog')
-export class GrConfirmRevertDialog extends LitElement {
+export class GrConfirmRevertDialog
+ extends LitElement
+ implements ChangeActionDialog
+{
/* The revert message updated by the user
The default value is set by the dialog */
@state()
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index 1c48a42..1b5f171 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -8,7 +8,7 @@
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import '../../plugins/gr-endpoint-param/gr-endpoint-param';
import '../gr-thread-list/gr-thread-list';
-import {ActionInfo, EDIT} from '../../../types/common';
+import {ActionInfo, ChangeActionDialog, EDIT} from '../../../types/common';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {pluralize} from '../../../utils/string-util';
import {CommentThread, isUnresolved} from '../../../utils/comment-util';
@@ -23,7 +23,10 @@
import {resolve} from '../../../models/dependency';
@customElement('gr-confirm-submit-dialog')
-export class GrConfirmSubmitDialog extends LitElement {
+export class GrConfirmSubmitDialog
+ extends LitElement
+ implements ChangeActionDialog
+{
@query('#dialog')
dialog?: GrDialog;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
index 89a1ff5..c1e866c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.ts
@@ -20,8 +20,6 @@
PatchSetNum,
CommitInfo,
ServerInfo,
- RevisionInfo,
- NumericChangeId,
BasePatchSetNum,
} from '../../../types/common';
import {DiffPreferencesInfo} from '../../../types/diff';
@@ -71,9 +69,6 @@
change: ChangeInfo | undefined;
@property({type: String})
- changeNum?: NumericChangeId;
-
- @property({type: String})
changeUrl?: string;
@property({type: Object})
@@ -97,9 +92,6 @@
@property({type: String})
filesExpanded?: FilesExpandedState;
- @property({type: Object})
- revisionInfo?: RevisionInfo;
-
@state()
diffPrefs?: DiffPreferencesInfo;
@@ -274,12 +266,6 @@
<div class="patchInfoContent">
<gr-patch-range-select
id="rangeSelect"
- .changeNum=${this.changeNum}
- .patchNum=${this.patchNum}
- .basePatchNum=${this.basePatchNum}
- .availablePatches=${this.allPatchSets}
- .revisions=${this.change.revisions}
- .revisionInfo=${this.revisionInfo}
@patch-range-change=${this.handlePatchChange}
>
</gr-patch-range-select>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
index 23534a0..6c2282b 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.ts
@@ -18,7 +18,6 @@
import {
BasePatchSetNum,
ChangeId,
- NumericChangeId,
PARENT,
PatchSetNum,
PatchSetNumber,
@@ -174,7 +173,6 @@
});
test('show/hide diffs disabled for large amounts of files', async () => {
- element.changeNum = 42 as NumericChangeId;
element.basePatchNum = PARENT;
element.patchNum = '2' as PatchSetNum;
element.shownFileCount = 1;
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 071c489..d4defcb 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
@@ -78,9 +78,11 @@
import {incrementalRepeat} from '../../lit/incremental-repeat';
import {ifDefined} from 'lit/directives/if-defined.js';
import {HtmlPatched} from '../../../utils/lit-util';
-import {createDiffUrl} from '../../../models/views/diff';
-import {createEditUrl} from '../../../models/views/edit';
-import {createChangeUrl} from '../../../models/views/change';
+import {
+ createDiffUrl,
+ createEditUrl,
+ createChangeUrl,
+} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
import {FileMode, fileModeToString} from '../../../utils/file-util';
@@ -758,7 +760,7 @@
);
subscribe(
this,
- () => this.getFilesModel().filesWithUnmodified$,
+ () => this.getFilesModel().filesIncludingUnmodified$,
files => {
this.files = [...files];
}
@@ -2121,9 +2123,9 @@
this.getNavigation().setUrl(
createDiffUrl({
change: this.change,
- path: diff.path,
patchNum: this.patchRange.patchNum,
basePatchNum: this.patchRange.basePatchNum,
+ diffView: {path: diff.path},
})
);
}
@@ -2142,9 +2144,9 @@
this.getNavigation().setUrl(
createDiffUrl({
change: this.change,
- path: this.files[this.fileCursor.index].__path,
patchNum: this.patchRange.patchNum,
basePatchNum: this.patchRange.basePatchNum,
+ diffView: {path: this.files[this.fileCursor.index].__path},
})
);
}
@@ -2176,16 +2178,16 @@
return createEditUrl({
changeNum: this.change._number,
repo: this.change.project,
- path,
patchNum: this.patchRange.patchNum,
+ editView: {path},
});
}
return createDiffUrl({
changeNum: this.change._number,
repo: this.change.project,
- path,
patchNum: this.patchRange.patchNum,
basePatchNum: this.patchRange.basePatchNum,
+ diffView: {path},
});
}
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 767c4cf..218594b 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
@@ -121,7 +121,6 @@
import {subscribe} from '../../lit/subscription-controller';
import {configModelToken} from '../../../models/config/config-model';
import {hasHumanReviewer, isOwner} from '../../../utils/change-util';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {
CommentEditingChangedDetail,
@@ -384,8 +383,6 @@
private readonly restApiService: RestApiService =
getAppContext().restApiService;
- private readonly flagsService = getAppContext().flagsService;
-
private readonly getPluginLoader = resolve(this, pluginLoaderToken);
private readonly getConfigModel = resolve(this, configModelToken);
@@ -671,9 +668,6 @@
this,
() => this.getCommentsModel().mentionedUsersInUnresolvedDrafts$,
x => {
- if (!this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
- return;
- }
this.mentionedUsersInUnresolvedDrafts = x.filter(
v => !this.isAlreadyReviewerOrCC(v)
);
@@ -1441,18 +1435,13 @@
).filter(isDefined);
for (const user of newAttentionSetUsers) {
- let reason;
- if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
- reason =
- getMentionedReason(
- this.draftCommentThreads,
- this.account,
- user,
- this.serverConfig
- ) ?? '';
- } else {
- reason = getReplyByReason(this.account, this.serverConfig);
- }
+ const reason =
+ getMentionedReason(
+ this.draftCommentThreads,
+ this.account,
+ user,
+ this.serverConfig
+ ) ?? '';
reviewInput.add_to_attention_set.push({user: getUserId(user), reason});
}
reviewInput.remove_from_attention_set = [];
@@ -1943,6 +1932,7 @@
}
confirmPendingReviewer() {
+ this.reviewerConfirmationModal?.close();
if (this.ccPendingConfirmation) {
this.ccsList?.confirmGroup(this.ccPendingConfirmation.group);
this.focusOn(FocusTarget.CCS);
@@ -1960,6 +1950,7 @@
}
cancelPendingReviewer() {
+ this.reviewerConfirmationModal?.close();
this.ccPendingConfirmation = null;
this.reviewerPendingConfirmation = null;
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 acb7c83..f7b3aec 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
@@ -13,7 +13,6 @@
query,
queryAll,
queryAndAssert,
- stubFlags,
stubRestApi,
waitUntilVisible,
} from '../../../test/test-utils';
@@ -55,12 +54,10 @@
import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
import {GrLabelScores} from '../gr-label-scores/gr-label-scores';
import {GrThreadList} from '../gr-thread-list/gr-thread-list';
-import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {fixture, html, waitUntil, assert} from '@open-wc/testing';
import {accountKey} from '../../../utils/account-util';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrAccountLabel} from '../../shared/gr-account-label/gr-account-label';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {Key, Modifier} from '../../../utils/dom-util';
import {GrComment} from '../../shared/gr-comment/gr-comment';
import {testResolver} from '../../../test/common-test-setup';
@@ -1284,40 +1281,6 @@
});
});
- function getActiveElement() {
- return document.activeElement;
- }
-
- function overlayObserver(mode: string) {
- return new Promise(resolve => {
- function listener() {
- element.removeEventListener('iron-overlay-' + mode, listener);
- resolve(mode);
- }
- element.addEventListener('iron-overlay-' + mode, listener);
- });
- }
-
- function isFocusInsideElement(element: Element) {
- // In Polymer 2 focused element either <paper-input> or nested
- // native input <input> element depending on the current focus
- // in browser window.
- // For example, the focus is changed if the developer console
- // get a focus.
- let activeElement = getActiveElement();
- while (activeElement) {
- if (activeElement === element) {
- return true;
- }
- if (activeElement.parentElement) {
- activeElement = activeElement.parentElement;
- } else {
- activeElement = (activeElement.getRootNode() as ShadowRoot).host;
- }
- }
- return false;
- }
-
async function testConfirmationDialog(cc?: boolean) {
const yesButton = queryAndAssert<GrButton>(
element,
@@ -1332,11 +1295,9 @@
element.reviewerPendingConfirmation = null;
await element.updateComplete;
assert.isFalse(
- isVisible(queryAndAssert(element, 'reviewerConfirmationModal'))
+ isVisible(queryAndAssert(element, '#reviewerConfirmationModal'))
);
- // Cause the confirmation dialog to display.
- let observer = overlayObserver('opened');
const group = {
id: 'id' as GroupId,
name: 'name' as GroupName,
@@ -1345,13 +1306,13 @@
element.ccPendingConfirmation = {
group,
confirm: false,
- count: 1,
+ count: 10,
};
} else {
element.reviewerPendingConfirmation = {
group,
confirm: false,
- count: 1,
+ count: 10,
};
}
await element.updateComplete;
@@ -1368,40 +1329,35 @@
);
}
- await observer;
assert.isTrue(
- isVisible(queryAndAssert(element, 'reviewerConfirmationModal'))
+ isVisible(queryAndAssert(element, '#reviewerConfirmationModal'))
);
- observer = overlayObserver('closed');
const expected = 'Group name has 10 members';
assert.notEqual(
queryAndAssert<HTMLElement>(
element,
- 'reviewerConfirmationModal'
+ '#reviewerConfirmationModal'
).innerText.indexOf(expected),
-1
);
- noButton.click(); // close the overlay
-
- await observer;
- assert.isFalse(
- isVisible(queryAndAssert(element, 'reviewerConfirmationModal'))
+ noButton.click(); // close the dialog
+ await waitUntil(
+ () => !isVisible(queryAndAssert(element, '#reviewerConfirmationModal'))
);
+ // TODO(dhruvsri): figure out why focus is not on the input element
// We should be focused on account entry input.
- const reviewersEntry = queryAndAssert<GrAccountList>(element, '#reviewers');
- assert.isTrue(
- isFocusInsideElement(
- queryAndAssert<GrAutocomplete>(reviewersEntry.entry, '#input').input!
- )
- );
+ // const reviewersEntry = queryAndAssert<GrAccountList>(element, '#reviewers');
+ // assert.isTrue(
+ // isFocusInsideElement(
+ // queryAndAssert<GrAutocomplete>(reviewersEntry.entry, '#input').input!
+ // )
+ // );
// No reviewer/CC should have been added.
assert.equal(element.ccsList?.additions().length, 0);
assert.equal(element.reviewersList?.additions().length, 0);
- // Reopen confirmation dialog.
- observer = overlayObserver('opened');
if (cc) {
element.ccPendingConfirmation = {
group,
@@ -1415,48 +1371,57 @@
count: 1,
};
}
+ await element.updateComplete;
- await observer;
assert.isTrue(
- isVisible(queryAndAssert(element, 'reviewerConfirmationModal'))
+ isVisible(queryAndAssert(element, '#reviewerConfirmationModal'))
);
- observer = overlayObserver('closed');
- yesButton.click(); // Confirm the group.
- await observer;
- assert.isFalse(
- isVisible(queryAndAssert(element, 'reviewerConfirmationModal'))
+ yesButton.click(); // Confirm the group.
+ await waitUntil(
+ () => !isVisible(queryAndAssert(element, '#reviewerConfirmationModal'))
);
const additions = cc
? element.ccsList?.additions()
: element.reviewersList?.additions();
assert.deepEqual(additions, [
{
+ confirmed: true,
+ id: 'id' as GroupId,
name: 'name' as GroupName,
},
]);
// We should be focused on account entry input.
- if (cc) {
- const ccsEntry = queryAndAssert<GrAccountList>(element, '#ccs');
- assert.isTrue(
- isFocusInsideElement(
- queryAndAssert<GrAutocomplete>(ccsEntry.entry, '#input').input!
- )
- );
- } else {
- const reviewersEntry = queryAndAssert<GrAccountList>(
- element,
- '#reviewers'
- );
- assert.isTrue(
- isFocusInsideElement(
- queryAndAssert<GrAutocomplete>(reviewersEntry.entry, '#input').input!
- )
- );
- }
+ // TODO(dhruvsri): figure out why focus is not on the input element
+ // if (cc) {
+ // const ccsEntry = queryAndAssert<GrAccountList>(element, '#ccs');
+ // assert.isTrue(
+ // isFocusInsideElement(
+ // queryAndAssert<GrAutocomplete>(ccsEntry.entry, '#input').input!
+ // )
+ // );
+ // } else {
+ // const reviewersEntry = queryAndAssert<GrAccountList>(
+ // element,
+ // '#reviewers'
+ // );
+ // assert.isTrue(
+ // isFocusInsideElement(
+ // queryAndAssert<GrAutocomplete>(reviewersEntry.entry, '#input').input!
+ // )
+ // );
+ // }
}
+ test('cc confirmation', async () => {
+ testConfirmationDialog(true);
+ });
+
+ test('reviewer confirmation', async () => {
+ testConfirmationDialog(false);
+ });
+
suite('reviewer toast for WIP changes', () => {
let fireStub: sinon.SinonStub;
setup(() => {
@@ -1521,14 +1486,6 @@
});
});
- test('cc confirmation', async () => {
- testConfirmationDialog(true);
- });
-
- test('reviewer confirmation', async () => {
- testConfirmationDialog(false);
- });
-
test('reviewersMutated when account-text-change is fired from ccs', () => {
assert.isFalse(element.reviewersMutated);
assert.isTrue(queryAndAssert<GrAccountList>(element, '#ccs').allowAnyInput);
@@ -2568,9 +2525,6 @@
suite('mention users', () => {
setup(async () => {
- stubFlags('isEnabled')
- .withArgs(KnownExperimentId.MENTION_USERS)
- .returns(true);
element.account = createAccountWithId(1);
element.requestUpdate();
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
index 80a1a9a..c09d2a2 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.ts
@@ -42,7 +42,6 @@
import {resolve} from '../../../models/dependency';
import {changeModelToken} from '../../../models/change/change-model';
import {Interaction} from '../../../constants/reporting';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {HtmlPatched} from '../../../utils/lit-util';
import {userModelToken} from '../../../models/user/user-model';
import {specialFilePathCompare} from '../../../utils/path-list-util';
@@ -205,8 +204,6 @@
private readonly reporting = getAppContext().reportingService;
- private readonly flagsService = getAppContext().flagsService;
-
private readonly getUserModel = resolve(this, userModelToken);
private readonly patched = new HtmlPatched(key => {
@@ -495,14 +492,10 @@
value: CommentTabState.UNRESOLVED,
});
if (this.account) {
- if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
- items.push({
- text: `Mentions (${
- getMentionedThreads(threads, this.account).length
- })`,
- value: CommentTabState.MENTIONS,
- });
- }
+ items.push({
+ text: `Mentions (${getMentionedThreads(threads, this.account).length})`,
+ value: CommentTabState.MENTIONS,
+ });
items.push({
text: `Drafts (${threads.filter(isDraftThread).length})`,
value: CommentTabState.DRAFTS,
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 3fdb1c0..5792230 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -75,8 +75,7 @@
import {HtmlPatched} from '../../utils/lit-util';
import {DropdownItem} from '../shared/gr-dropdown-list/gr-dropdown-list';
import './gr-checks-attempt';
-import {createDiffUrl} from '../../models/views/diff';
-import {changeViewModelToken} from '../../models/views/change';
+import {createDiffUrl, changeViewModelToken} from '../../models/views/change';
/**
* Firing this event sets the regular expression of the results filter.
@@ -715,9 +714,8 @@
url: createDiffUrl({
changeNum: change._number,
repo: change.project,
- path,
patchNum: patchset,
- lineNum: line,
+ diffView: {path, lineNum: line},
}),
primary: true,
};
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
index 0a21da4..efc6efe 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result.ts
@@ -8,11 +8,21 @@
import {LitElement, css, html, PropertyValues, nothing} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {RunResult} from '../../models/checks/checks-model';
-import {createFixAction, iconFor} from '../../models/checks/checks-util';
+import {
+ createFixAction,
+ createPleaseFixComment,
+ iconFor,
+} from '../../models/checks/checks-util';
import {modifierPressed} from '../../utils/dom-util';
import './gr-checks-results';
import './gr-hovercard-run';
import {fontStyles} from '../../styles/gr-font-styles';
+import {Action} from '../../api/checks';
+import {assertIsDefined} from '../../utils/common-util';
+import {resolve} from '../../models/dependency';
+import {commentsModelToken} from '../../models/comments/comments-model';
+import {subscribe} from '../lit/subscription-controller';
+import {changeModelToken} from '../../models/change/change-model';
@customElement('gr-diff-check-result')
export class GrDiffCheckResult extends LitElement {
@@ -32,6 +42,13 @@
@state()
isExpandable = false;
+ @state()
+ isOwner = false;
+
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
+ private readonly getCommentsModel = resolve(this, commentsModelToken);
+
static override get styles() {
return [
fontStyles,
@@ -114,6 +131,15 @@
];
}
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getChangeModel().isOwner$,
+ x => (this.isOwner = x)
+ );
+ }
+
override render() {
if (!this.result) return;
const cat = this.result.category.toLowerCase();
@@ -182,14 +208,39 @@
private renderActions() {
if (!this.isExpanded) return nothing;
- return html`<div class="actions">${this.renderFixButton()}</div>`;
+ return html`<div class="actions">
+ ${this.renderPleaseFixButton()}${this.renderShowFixButton()}
+ </div>`;
}
- private renderFixButton() {
+ private renderPleaseFixButton() {
+ if (this.isOwner) return nothing;
+ const action: Action = {
+ name: 'Please Fix',
+ callback: () => {
+ assertIsDefined(this.result, 'result');
+ this.getCommentsModel().saveDraft(createPleaseFixComment(this.result));
+ return undefined;
+ },
+ };
+ return html`
+ <gr-checks-action
+ id="please-fix"
+ context="diff-fix"
+ .action=${action}
+ ></gr-checks-action>
+ `;
+ }
+
+ private renderShowFixButton() {
const action = createFixAction(this, this.result);
if (!action) return nothing;
return html`
- <gr-checks-action context="diff-fix" .action=${action}></gr-checks-action>
+ <gr-checks-action
+ id="show-fix"
+ context="diff-fix"
+ .action=${action}
+ ></gr-checks-action>
`;
}
diff --git a/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts b/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
index 3892c9a..0377e0e 100644
--- a/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
+++ b/polygerrit-ui/app/elements/checks/gr-diff-check-result_test.ts
@@ -7,6 +7,7 @@
import {fakeRun1} from '../../models/checks/checks-fakes';
import {RunResult} from '../../models/checks/checks-model';
import '../../test/common-test-setup';
+import {queryAndAssert} from '../../utils/common-util';
import './gr-diff-check-result';
import {GrDiffCheckResult} from './gr-diff-check-result';
@@ -50,4 +51,30 @@
`
);
});
+
+ test('renders expanded', async () => {
+ element.result = {...fakeRun1, ...fakeRun1.results?.[2]} as RunResult;
+ element.isExpanded = true;
+ await element.updateComplete;
+
+ const details = queryAndAssert(element, 'div.details');
+ assert.dom.equal(
+ details,
+ /* HTML */ `
+ <div class="details">
+ <gr-result-expanded hidecodepointers=""></gr-result-expanded>
+ <div class="actions">
+ <gr-checks-action
+ id="please-fix"
+ context="diff-fix"
+ ></gr-checks-action>
+ <gr-checks-action
+ id="show-fix"
+ context="diff-fix"
+ ></gr-checks-action>
+ </div>
+ </div>
+ `
+ );
+ });
});
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index 2b7d1ff..1176ce3 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -5,7 +5,6 @@
*/
import '../gr-error-dialog/gr-error-dialog';
import '../../shared/gr-alert/gr-alert';
-import '../../shared/gr-overlay/gr-overlay';
import {getBaseUrl} from '../../../utils/url-util';
import {getAppContext} from '../../../services/app-context';
import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index 1c4e237..833a91a 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -204,18 +204,22 @@
text-decoration: underline;
}
.titleText::before {
+ --icon-width: var(--header-icon-width, var(--header-icon-size, 0));
+ --icon-height: var(--header-icon-height, var(--header-icon-size, 0));
background-image: var(--header-icon);
- background-size: var(--header-icon-size) var(--header-icon-size);
+ background-size: var(--icon-width) var(--icon-height);
background-repeat: no-repeat;
content: '';
display: inline-block;
- height: var(--header-icon-size);
- margin-right: calc(var(--header-icon-size) / 4);
+ height: var(--icon-height);
+ /* If size or height are set, then use 'spacing-m', 0px otherwise. */
+ margin-right: clamp(0px, var(--icon-height), var(--spacing-m));
vertical-align: text-bottom;
- width: var(--header-icon-size);
+ width: var(--icon-width);
}
.titleText::after {
content: var(--header-title-content);
+ white-space: nowrap;
}
ul {
list-style: none;
@@ -424,17 +428,19 @@
private renderAccount() {
return html`
<div class="accountContainer" id="accountContainer">
- <gr-icon
- id="mobileSearch"
- icon="search"
- @click=${(e: Event) => {
- this.onMobileSearchTap(e);
- }}
- role="button"
- aria-label=${this.mobileSearchHidden
- ? 'Show Searchbar'
- : 'Hide Searchbar'}
- ></gr-icon>
+ <div>
+ <gr-icon
+ id="mobileSearch"
+ icon="search"
+ @click=${(e: Event) => {
+ this.onMobileSearchTap(e);
+ }}
+ role="button"
+ aria-label=${this.mobileSearchHidden
+ ? 'Show Searchbar'
+ : 'Hide Searchbar'}
+ ></gr-icon>
+ </div>
${this.renderRegister()}
<a class="loginButton" href=${this.loginUrl}>Sign in</a>
<a
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index bca2634..7eb19f0 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -75,13 +75,15 @@
</gr-endpoint-decorator>
</div>
<div class="accountContainer" id="accountContainer">
- <gr-icon
- aria-label="Hide Searchbar"
- icon="search"
- id="mobileSearch"
- role="button"
- >
- </gr-icon>
+ <div>
+ <gr-icon
+ aria-label="Hide Searchbar"
+ icon="search"
+ id="mobileSearch"
+ role="button"
+ >
+ </gr-icon>
+ </div>
<a class="loginButton" href="/login"> Sign in </a>
<a
aria-label="Settings"
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 6caa000..bcf6937 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -10,7 +10,11 @@
} from '../../../utils/page-wrapper-utils';
import {NavigationService} from '../gr-navigation/gr-navigation';
import {getAppContext} from '../../../services/app-context';
-import {convertToPatchSetNum} from '../../../utils/patch-set-util';
+import {
+ computeAllPatchSets,
+ computeLatestPatchNum,
+ convertToPatchSetNum,
+} from '../../../utils/patch-set-util';
import {assertIsDefined} from '../../../utils/common-util';
import {
BasePatchSetNum,
@@ -27,7 +31,7 @@
import {AppElement, AppElementParams} from '../../gr-app-types';
import {LocationChangeEventDetail} from '../../../types/events';
import {GerritView, RouterModel} from '../../../services/router/router-model';
-import {firePageError} from '../../../utils/event-util';
+import {fireAlert, firePageError} from '../../../utils/event-util';
import {windowLocationReload} from '../../../utils/dom-util';
import {
getBaseUrl,
@@ -56,17 +60,18 @@
RepoViewState,
} from '../../../models/views/repo';
import {
+ createGroupUrl,
GroupDetailView,
GroupViewModel,
GroupViewState,
} from '../../../models/views/group';
-import {DiffViewModel, DiffViewState} from '../../../models/views/diff';
import {
+ ChangeChildView,
ChangeViewModel,
ChangeViewState,
- createChangeUrl,
+ createChangeViewUrl,
+ createDiffUrl,
} from '../../../models/views/change';
-import {EditViewModel, EditViewState} from '../../../models/views/edit';
import {
DashboardViewModel,
DashboardViewState,
@@ -87,6 +92,13 @@
import {SearchViewModel, SearchViewState} from '../../../models/views/search';
import {DashboardSection} from '../../../utils/dashboard-util';
import {Subscription} from 'rxjs';
+import {
+ addPath,
+ findComment,
+ getPatchRangeForCommentUrl,
+ isInBaseOfPatchRange,
+} from '../../../utils/comment-util';
+import {isFileUnchanged} from '../../../embed/diff/gr-diff/gr-diff-utils';
const RoutePattern = {
ROOT: '/',
@@ -302,9 +314,7 @@
private readonly agreementViewModel: AgreementViewModel,
private readonly changeViewModel: ChangeViewModel,
private readonly dashboardViewModel: DashboardViewModel,
- private readonly diffViewModel: DiffViewModel,
private readonly documentationViewModel: DocumentationViewModel,
- private readonly editViewModel: EditViewModel,
private readonly groupViewModel: GroupViewModel,
private readonly pluginViewModel: PluginViewModel,
private readonly repoViewModel: RepoViewModel,
@@ -321,7 +331,7 @@
// So this check is slightly fragile, but should work.
if (this.view !== GerritView.CHANGE) return;
const browserUrl = new URL(window.location.toString());
- const stateUrl = new URL(createChangeUrl(state), browserUrl);
+ const stateUrl = new URL(createChangeViewUrl(state), browserUrl);
// Keeping the hash and certain parameters are stop-gap solution. We
// should find better ways of maintaining an overall consistent URL
@@ -362,13 +372,14 @@
if ('repo' in state && state.repo !== undefined && 'changeNum' in state)
this.restApiService.setInProjectLookup(state.changeNum, state.repo);
- this.routerModel.setState({
- view: state.view,
- changeNum: 'changeNum' in state ? state.changeNum : undefined,
- patchNum: 'patchNum' in state ? state.patchNum ?? undefined : undefined,
- basePatchNum:
- 'basePatchNum' in state ? state.basePatchNum ?? undefined : undefined,
- });
+ this.routerModel.setState({view: state.view});
+ // We are trying to reset the change (view) model when navigating to other
+ // views, because we don't trust our reset logic at the moment. The models
+ // singletons and might unintentionally keep state from one change to
+ // another. TODO: Let's find some way to avoid that.
+ if (state.view !== GerritView.CHANGE) {
+ this.changeViewModel.setState(undefined);
+ }
this.appElement().params = state;
}
@@ -1067,7 +1078,8 @@
}
handleGroupInfoRoute(ctx: PageContext) {
- this.redirect('/admin/groups/' + encodeURIComponent(ctx.params[0]));
+ const groupId = ctx.params[0] as GroupId;
+ this.redirect(createGroupUrl({groupId}));
}
handleGroupSelfRedirectRoute(_: PageContext) {
@@ -1151,6 +1163,8 @@
}
}
+ // TODO: Change the route pattern to match `repo` and `detailView`
+ // separately, and then use `createRepoUrl()` here.
this.redirect(`/admin/repos/${params}`);
}
@@ -1437,6 +1451,7 @@
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]) as RevisionPatchSetNum,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
};
const queryMap = new URLSearchParams(ctx.querystring);
@@ -1467,21 +1482,57 @@
this.changeViewModel.setState(state);
}
- handleCommentRoute(ctx: PageContext) {
+ async handleCommentRoute(ctx: PageContext) {
const changeNum = Number(ctx.params[1]) as NumericChangeId;
- const state: DiffViewState = {
- repo: ctx.params[0] as RepoName,
+ const repo = ctx.params[0] as RepoName;
+ const commentId = ctx.params[2] as UrlEncodedCommentId;
+
+ const comments = await this.restApiService.getDiffComments(changeNum);
+ const change = await this.restApiService.getChangeDetail(changeNum);
+
+ const comment = findComment(addPath(comments), commentId);
+ const path = comment?.path;
+ const patchsets = computeAllPatchSets(change);
+ const latestPatchNum = computeLatestPatchNum(patchsets);
+ if (!comment || !path || !latestPatchNum) {
+ this.show404();
+ return;
+ }
+ let {basePatchNum, patchNum} = getPatchRangeForCommentUrl(
+ comment,
+ latestPatchNum
+ );
+
+ if (basePatchNum !== PARENT) {
+ const diff = await this.restApiService.getDiff(
+ changeNum,
+ basePatchNum,
+ patchNum,
+ path
+ );
+ if (diff && isFileUnchanged(diff)) {
+ fireAlert(
+ document,
+ `File is unchanged between Patchset ${basePatchNum} and ${patchNum}.
+ Showing diff of Base vs ${basePatchNum}.`
+ );
+ patchNum = basePatchNum as RevisionPatchSetNum;
+ basePatchNum = PARENT;
+ }
+ }
+
+ const diffUrl = createDiffUrl({
changeNum,
- commentId: ctx.params[2] as UrlEncodedCommentId,
- view: GerritView.DIFF,
- commentLink: true,
- };
- this.reporting.setRepoName(state.repo ?? '');
- this.reporting.setChangeId(changeNum);
- this.normalizePatchRangeParams(state);
- // Note that router model view must be updated before view models.
- this.setState(state);
- this.diffViewModel.setState(state);
+ repo,
+ patchNum,
+ basePatchNum,
+ diffView: {
+ path,
+ lineNum: comment.line,
+ leftSide: isInBaseOfPatchRange(comment, {basePatchNum, patchNum}),
+ },
+ });
+ this.redirect(diffUrl);
}
handleCommentsRoute(ctx: PageContext) {
@@ -1491,6 +1542,7 @@
changeNum,
commentId: ctx.params[2] as UrlEncodedCommentId,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
};
assertIsDefined(state.repo);
this.reporting.setRepoName(state.repo);
@@ -1504,25 +1556,26 @@
handleDiffRoute(ctx: PageContext) {
const changeNum = Number(ctx.params[1]) as NumericChangeId;
// Parameter order is based on the regex group number matched.
- const state: DiffViewState = {
+ const state: ChangeViewState = {
repo: ctx.params[0] as RepoName,
changeNum,
basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
patchNum: convertToPatchSetNum(ctx.params[6]) as RevisionPatchSetNum,
- path: ctx.params[8],
- view: GerritView.DIFF,
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.DIFF,
+ diffView: {path: ctx.params[8]},
};
const address = this.parseLineAddress(ctx.hash);
if (address) {
- state.leftSide = address.leftSide;
- state.lineNum = address.lineNum;
+ state.diffView!.leftSide = address.leftSide;
+ state.diffView!.lineNum = address.lineNum;
}
this.reporting.setRepoName(state.repo ?? '');
this.reporting.setChangeId(changeNum);
this.normalizePatchRangeParams(state);
// Note that router model view must be updated before view models.
this.setState(state);
- this.diffViewModel.setState(state);
+ this.changeViewModel.setState(state);
}
handleChangeLegacyRoute(ctx: PageContext) {
@@ -1550,19 +1603,19 @@
// Parameter order is based on the regex group number matched.
const project = ctx.params[0] as RepoName;
const changeNum = Number(ctx.params[1]) as NumericChangeId;
- const state: EditViewState = {
+ const state: ChangeViewState = {
repo: project,
changeNum,
// for edit view params, patchNum cannot be undefined
patchNum: convertToPatchSetNum(ctx.params[2]) as RevisionPatchSetNum,
- path: ctx.params[3],
- lineNum: Number(ctx.hash),
- view: GerritView.EDIT,
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.EDIT,
+ editView: {path: ctx.params[3], lineNum: Number(ctx.hash)},
};
this.normalizePatchRangeParams(state);
// Note that router model view must be updated before view models.
this.setState(state);
- this.editViewModel.setState(state);
+ this.changeViewModel.setState(state);
this.reporting.setRepoName(project);
this.reporting.setChangeId(changeNum);
}
@@ -1577,6 +1630,7 @@
changeNum,
patchNum: convertToPatchSetNum(ctx.params[3]) as RevisionPatchSetNum,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
edit: true,
};
const tab = queryMap.get('tab');
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index b8f68e6..d8761bf 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -28,10 +28,16 @@
import {AdminChildView} from '../../../models/views/admin';
import {RepoDetailView} from '../../../models/views/repo';
import {GroupDetailView} from '../../../models/views/group';
-import {EditViewState} from '../../../models/views/edit';
-import {ChangeViewState} from '../../../models/views/change';
+import {ChangeChildView, ChangeViewState} from '../../../models/views/change';
import {PatchRangeParams} from '../../../utils/url-util';
import {testResolver} from '../../../test/common-test-setup';
+import {
+ createComment,
+ createDiff,
+ createParsedChange,
+ createRevision,
+} from '../../../test/test-data-generators';
+import {ParsedChangeInfo} from '../../../types/types';
suite('gr-router tests', () => {
let router: GrRouter;
@@ -1134,6 +1140,7 @@
const ctx = makeParams('', '');
assertctxToParams(ctx, 'handleChangeRoute', {
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
@@ -1154,6 +1161,7 @@
ctx.querystring = queryMap.toString();
assertctxToParams(ctx, 'handleChangeRoute', {
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
@@ -1193,36 +1201,89 @@
test('diff view', () => {
const ctx = makeParams('foo/bar/baz', 'b44');
assertctxToParams(ctx, 'handleDiffRoute', {
- view: GerritView.DIFF,
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.DIFF,
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
basePatchNum: 4 as BasePatchSetNum,
patchNum: 7 as RevisionPatchSetNum,
- path: 'foo/bar/baz',
- leftSide: true,
- lineNum: 44,
+ diffView: {
+ path: 'foo/bar/baz',
+ lineNum: 44,
+ leftSide: true,
+ },
});
assert.isFalse(redirectStub.called);
});
- test('comment route', () => {
- const url = '/c/gerrit/+/264833/comment/00049681_f34fd6a9/';
+ test('comment route base..1', async () => {
+ const change: ParsedChangeInfo = createParsedChange();
+ const repo = change.project;
+ const changeNum = change._number;
+ const ps = 1 as RevisionPatchSetNum;
+ const line = 23;
+ const id = '00049681_f34fd6a9' as UrlEncodedCommentId;
+ stubRestApi('getChangeDetail').resolves(change);
+ stubRestApi('getDiffComments').resolves({
+ filepath: [{...createComment(), id, patch_set: ps, line}],
+ });
+
+ const url = `/c/${repo}/+/${changeNum}/comment/${id}/`;
const groups = url.match(_testOnly_RoutePattern.COMMENT);
- assert.deepEqual(groups!.slice(1), [
- 'gerrit', // project
- '264833', // changeNum
- '00049681_f34fd6a9', // commentId
- ]);
- assertctxToParams(
- {params: groups!.slice(1)} as any,
- 'handleCommentRoute',
- {
- repo: 'gerrit' as RepoName,
- changeNum: 264833 as NumericChangeId,
- commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
- commentLink: true,
- view: GerritView.DIFF,
- }
+ assert.deepEqual(groups!.slice(1), [repo, `${changeNum}`, id]);
+
+ await router.handleCommentRoute({params: groups!.slice(1)} as any);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(
+ redirectStub.lastCall.args[0],
+ `/c/${repo}/+/${changeNum}/${ps}/filepath#${line}`
+ );
+ });
+
+ test('comment route 1..2', async () => {
+ const change: ParsedChangeInfo = {
+ ...createParsedChange(),
+ revisions: {
+ abc: createRevision(1),
+ def: createRevision(2),
+ },
+ };
+ const repo = change.project;
+ const changeNum = change._number;
+ const ps = 1 as RevisionPatchSetNum;
+ const line = 23;
+ const id = '00049681_f34fd6a9' as UrlEncodedCommentId;
+
+ stubRestApi('getChangeDetail').resolves(change);
+ stubRestApi('getDiffComments').resolves({
+ filepath: [{...createComment(), id, patch_set: ps, line}],
+ });
+ const diffStub = stubRestApi('getDiff');
+
+ const url = `/c/${repo}/+/${changeNum}/comment/${id}/`;
+ const groups = url.match(_testOnly_RoutePattern.COMMENT);
+
+ // If getDiff() returns a diff with changes, then we will compare
+ // the patchset of the comment (1) against latest (2).
+ diffStub.onFirstCall().resolves(createDiff());
+ await router.handleCommentRoute({params: groups!.slice(1)} as any);
+ assert.isTrue(redirectStub.calledOnce);
+ assert.equal(
+ redirectStub.lastCall.args[0],
+ `/c/${repo}/+/${changeNum}/${ps}..2/filepath#b${line}`
+ );
+
+ // If getDiff() returns an unchanged diff, then we will compare
+ // the patchset of the comment (1) against base.
+ diffStub.onSecondCall().resolves({
+ ...createDiff(),
+ content: [],
+ });
+ await router.handleCommentRoute({params: groups!.slice(1)} as any);
+ assert.isTrue(redirectStub.calledTwice);
+ assert.equal(
+ redirectStub.lastCall.args[0],
+ `/c/${repo}/+/${changeNum}/${ps}/filepath#${line}`
);
});
@@ -1242,6 +1303,7 @@
changeNum: 264833 as NumericChangeId,
commentId: '00049681_f34fd6a9' as UrlEncodedCommentId,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
}
);
});
@@ -1259,13 +1321,13 @@
3: 'foo/bar/baz', // 3 File path
},
};
- const appParams: EditViewState = {
+ const appParams: ChangeViewState = {
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
- view: GerritView.EDIT,
- path: 'foo/bar/baz',
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.EDIT,
patchNum: 3 as RevisionPatchSetNum,
- lineNum: 0,
+ editView: {path: 'foo/bar/baz', lineNum: 0},
};
router.handleDiffEditRoute(ctx);
@@ -1285,13 +1347,13 @@
3: 'foo/bar/baz', // 3 File path
},
};
- const appParams: EditViewState = {
+ const appParams: ChangeViewState = {
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
- view: GerritView.EDIT,
- path: 'foo/bar/baz',
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.EDIT,
patchNum: 3 as RevisionPatchSetNum,
- lineNum: 4,
+ editView: {path: 'foo/bar/baz', lineNum: 4},
};
router.handleDiffEditRoute(ctx);
@@ -1314,6 +1376,7 @@
repo: 'foo/bar' as RepoName,
changeNum: 1234 as NumericChangeId,
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
patchNum: 3 as RevisionPatchSetNum,
edit: true,
};
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index bbca52d..b146e93 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -6,7 +6,6 @@
import '../../../styles/shared-styles';
import '../../shared/gr-dialog/gr-dialog';
import '../../shared/gr-icon/gr-icon';
-import '../../shared/gr-overlay/gr-overlay';
import '../../../embed/diff/gr-diff/gr-diff';
import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
index 26043b32..6eb5243 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.ts
@@ -7,7 +7,6 @@
PatchRange,
PatchSetNum,
RobotCommentInfo,
- UrlEncodedCommentId,
PathToCommentsInfoMap,
FileInfo,
PARENT,
@@ -64,26 +63,6 @@
return this._drafts;
}
- findCommentById(
- commentId?: UrlEncodedCommentId
- ): CommentInfo | DraftInfo | undefined {
- if (!commentId) return undefined;
- const findComment = (comments: {
- [path: string]: (CommentInfo | DraftInfo)[];
- }) => {
- let comment;
- for (const path of Object.keys(comments)) {
- comment = comment || comments[path].find(c => c.id === commentId);
- }
- return comment;
- };
- return (
- findComment(this._comments) ||
- findComment(this._robotComments) ||
- findComment(this._drafts)
- );
- }
-
/**
* Get an object mapping file paths to a boolean representing whether that
* path contains diff comments in the given patch set (including drafts and
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index eb2b494..23dce97 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -26,12 +26,7 @@
isInBaseOfPatchRange,
isInRevisionOfPatchRange,
} from '../../../utils/comment-util';
-import {
- CommitRange,
- CoverageRange,
- DiffLayer,
- PatchSetFile,
-} from '../../../types/types';
+import {CoverageRange, DiffLayer, PatchSetFile} from '../../../types/types';
import {
Base64ImageFile,
BlameInfo,
@@ -192,9 +187,6 @@
fire(this, 'is-image-diff-changed', {value: isImageDiff});
}
- @property({type: Object})
- commitRange?: CommitRange;
-
@state()
private _editWeblinks?: GeneratedWebLink[];
@@ -355,9 +347,7 @@
);
this.renderPrefs = {
...this.renderPrefs,
- use_lit_components: this.flags.isEnabled(
- KnownExperimentId.DIFF_RENDERING_LIT
- ),
+ use_lit_components: true,
};
this.addEventListener(
// These are named inconsistently for a reason:
@@ -607,7 +597,11 @@
this.hasReloadBeenCalledOnce = true;
this.reporting.time(Timing.DIFF_TOTAL);
this.reporting.time(Timing.DIFF_LOAD);
+ // TODO: Find better names for these 3 clear/cancel methods. Ideally the
+ // <gr-diff-host> should not re-used at all for another diff rendering pass.
this.clear();
+ this.cancel();
+ this.clearDiffContent();
assertIsDefined(this.path, 'path');
assertIsDefined(this.changeNum, 'changeNum');
this.diff = undefined;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index b68adf1..c74993f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -20,22 +20,12 @@
import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog';
import '../gr-patch-range-select/gr-patch-range-select';
import '../../change/gr-download-dialog/gr-download-dialog';
-import '../../shared/gr-overlay/gr-overlay';
-import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {getAppContext} from '../../../services/app-context';
+import {isMergeParent, getParentIndex} from '../../../utils/patch-set-util';
import {
- computeAllPatchSets,
- computeLatestPatchNum,
- PatchSet,
- isMergeParent,
- getParentIndex,
-} from '../../../utils/patch-set-util';
-import {
- addUnmodifiedFiles,
computeDisplayPath,
computeTruncatedPath,
isMagicPath,
- specialFilePathCompare,
} from '../../../utils/path-list-util';
import {changeBaseURL, changeIsOpen} from '../../../utils/change-util';
import {GrDiffHost} from '../../diff/gr-diff-host/gr-diff-host';
@@ -43,56 +33,36 @@
DropdownItem,
GrDropdownList,
} from '../../shared/gr-dropdown-list/gr-dropdown-list';
-import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
import {ChangeComments} from '../../diff/gr-comment-api/gr-comment-api';
import {
BasePatchSetNum,
- ChangeInfo,
- CommitId,
EDIT,
- FileInfo,
NumericChangeId,
PARENT,
PatchRange,
- PatchSetNum,
PatchSetNumber,
PreferencesInfo,
RepoName,
- RevisionInfo,
RevisionPatchSetNum,
ServerInfo,
} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
-import {
- CommitRange,
- EditRevisionInfo,
- FileRange,
- ParsedChangeInfo,
-} from '../../../types/types';
+import {FileRange, ParsedChangeInfo} from '../../../types/types';
import {FilesWebLinks} from '../gr-patch-range-select/gr-patch-range-select';
import {GrDiffCursor} from '../../../embed/diff/gr-diff-cursor/gr-diff-cursor';
import {CommentSide, DiffViewMode, Side} from '../../../constants/constants';
import {GrApplyFixDialog} from '../gr-apply-fix-dialog/gr-apply-fix-dialog';
-import {RevisionInfo as RevisionInfoObj} from '../../shared/revision-info/revision-info';
-import {
- CommentMap,
- getPatchRangeForCommentUrl,
- isInBaseOfPatchRange,
-} from '../../../utils/comment-util';
+import {CommentMap} from '../../../utils/comment-util';
import {
EventType,
OpenFixPreviewEvent,
ValueChangedEvent,
} from '../../../types/events';
import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
-import {
- GerritView,
- routerModelToken,
-} from '../../../services/router/router-model';
import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
import {Key, toggleClass, whenVisible} from '../../../utils/dom-util';
import {CursorMoveResult} from '../../../api/core';
-import {isFalse, throttleWrap, until} from '../../../utils/async-util';
+import {throttleWrap} from '../../../utils/async-util';
import {filter, take, switchMap} from 'rxjs/operators';
import {combineLatest} from 'rxjs';
import {
@@ -100,14 +70,12 @@
ShortcutSection,
shortcutsServiceToken,
} from '../../../services/shortcuts/shortcuts-service';
-import {LoadingStatus} from '../../../models/change/change-model';
import {DisplayLine} from '../../../api/diff';
import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
import {commentsModelToken} from '../../../models/comments/comments-model';
import {changeModelToken} from '../../../models/change/change-model';
import {resolve} from '../../../models/dependency';
-import {BehaviorSubject} from 'rxjs';
-import {css, html, LitElement, PropertyValues} from 'lit';
+import {css, html, LitElement, nothing, PropertyValues} from 'lit';
import {ShortcutController} from '../../lit/shortcut-controller';
import {subscribe} from '../../lit/subscription-controller';
import {customElement, property, query, state} from 'lit/decorators.js';
@@ -118,15 +86,18 @@
import {when} from 'lit/directives/when.js';
import {
createDiffUrl,
- diffViewModelToken,
- DiffViewState,
-} from '../../../models/views/diff';
-import {createChangeUrl} from '../../../models/views/change';
-import {createEditUrl} from '../../../models/views/edit';
+ ChangeChildView,
+ changeViewModelToken,
+} from '../../../models/views/change';
import {GeneratedWebLink} from '../../../utils/weblink-util';
import {userModelToken} from '../../../models/user/user-model';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {PaperTabsElement} from '@polymer/paper-tabs/paper-tabs';
+import {GrDiffPreferencesDialog} from '../gr-diff-preferences-dialog/gr-diff-preferences-dialog';
+import {
+ FileNameToNormalizedFileInfoMap,
+ filesModelToken,
+} from '../../../models/change/files-model';
const LOADING_BLAME = 'Loading blame...';
const LOADED_BLAME = 'Blame loaded';
@@ -136,14 +107,11 @@
// visible for testing
export interface Files {
- sortedFileList: string[];
- changeFilesByPath: {[path: string]: FileInfo};
+ /** All file paths sorted by `specialFilePathCompare`. */
+ sortedPaths: string[];
+ changeFilesByPath: FileNameToNormalizedFileInfoMap;
}
-interface CommentSkips {
- previous: string | null;
- next: string | null;
-}
@customElement('gr-diff-view')
export class GrDiffView extends LitElement {
/**
@@ -160,8 +128,8 @@
@query('#diffHost')
diffHost?: GrDiffHost;
- @query('#reviewed')
- reviewed?: HTMLInputElement;
+ @state()
+ reviewed = false;
@query('#downloadModal')
downloadModal?: HTMLDialogElement;
@@ -176,35 +144,33 @@
applyFixDialog?: GrApplyFixDialog;
@query('#diffPreferencesDialog')
- diffPreferencesDialog?: GrOverlay;
+ diffPreferencesDialog?: GrDiffPreferencesDialog;
- private _viewState: DiffViewState | undefined;
-
+ // Private but used in tests.
@state()
- get viewState(): DiffViewState | undefined {
- return this._viewState;
- }
-
- set viewState(viewState: DiffViewState | undefined) {
- if (this._viewState === viewState) return;
- const oldViewState = this._viewState;
- this._viewState = viewState;
- this.viewStateChanged();
- this.requestUpdate('viewState', oldViewState);
+ get patchRange(): PatchRange | undefined {
+ if (!this.patchNum) return undefined;
+ return {
+ patchNum: this.patchNum,
+ basePatchNum: this.basePatchNum,
+ };
}
// Private but used in tests.
@state()
- patchRange?: PatchRange;
+ patchNum?: RevisionPatchSetNum;
// Private but used in tests.
@state()
- commitRange?: CommitRange;
+ basePatchNum: BasePatchSetNum = PARENT;
// Private but used in tests.
@state()
change?: ParsedChangeInfo;
+ @state()
+ latestPatchNum?: PatchSetNumber;
+
// Private but used in tests.
@state()
changeComments?: ChangeComments;
@@ -217,10 +183,9 @@
@state()
diff?: DiffInfo;
- // TODO: Move to using files-model.
// Private but used in tests.
@state()
- files: Files = {sortedFileList: [], changeFilesByPath: {}};
+ files: Files = {sortedPaths: [], changeFilesByPath: {}};
// Private but used in tests
// Use path getter/setter.
@@ -238,13 +203,13 @@
this.requestUpdate('path', oldPath);
}
+ /** Allows us to react when the user switches to the DIFF view. */
// Private but used in tests.
- @state()
- loggedIn = false;
+ @state() isActiveChildView = false;
// Private but used in tests.
@state()
- loading = true;
+ loggedIn = false;
@property({type: Object})
prefs?: DiffPreferencesInfo;
@@ -267,65 +232,61 @@
// Private but used in tests.
@state()
- commentMap?: CommentMap;
-
- @state()
- private commentSkips?: CommentSkips;
-
- // Private but used in tests.
- @state()
isBlameLoaded?: boolean;
@state()
private isBlameLoading = false;
- @state()
- private allPatchSets?: PatchSet[] = [];
-
+ /** Directly reflects the view model property `diffView.lineNum`. */
// Private but used in tests.
@state()
focusLineNum?: number;
+ /** Directly reflects the view model property `diffView.leftSide`. */
+ @state()
+ leftSide = false;
+
// visible for testing
reviewedFiles = new Set<string>();
private readonly reporting = getAppContext().reportingService;
- private readonly restApiService = getAppContext().restApiService;
-
- private readonly getRouterModel = resolve(this, routerModelToken);
-
private readonly getUserModel = resolve(this, userModelToken);
private readonly getChangeModel = resolve(this, changeModelToken);
private readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getFilesModel = resolve(this, filesModelToken);
+
private readonly getShortcutsService = resolve(this, shortcutsServiceToken);
private readonly getConfigModel = resolve(this, configModelToken);
- private readonly getViewModel = resolve(this, diffViewModelToken);
+ private readonly getViewModel = resolve(this, changeViewModelToken);
private throttledToggleFileReviewed?: (e: KeyboardEvent) => void;
@state()
cursor?: GrDiffCursor;
- private connected$ = new BehaviorSubject(false);
-
private readonly shortcutsController = new ShortcutController(this);
- private readonly getNavigation = resolve(this, navigationToken);
-
constructor() {
super();
this.setupKeyboardShortcuts();
this.setupSubscriptions();
subscribe(
this,
- () => this.getViewModel().state$,
- x => (this.viewState = x)
+ () => this.getFilesModel().filesIncludingUnmodified$,
+ files => {
+ const filesByPath: FileNameToNormalizedFileInfoMap = {};
+ for (const f of files) filesByPath[f.__path] = f;
+ this.files = {
+ sortedPaths: files.map(f => f.__path),
+ changeFilesByPath: filesByPath,
+ };
+ }
);
}
@@ -339,10 +300,10 @@
listen(Shortcut.PREV_LINE, _ => this.handlePrevLine());
listen(Shortcut.VISIBLE_LINE, _ => this.cursor?.moveToVisibleArea());
listen(Shortcut.NEXT_FILE_WITH_COMMENTS, _ =>
- this.moveToNextFileWithComment()
+ this.moveToFileWithComment(1)
);
listen(Shortcut.PREV_FILE_WITH_COMMENTS, _ =>
- this.moveToPreviousFileWithComment()
+ this.moveToFileWithComment(-1)
);
listen(Shortcut.NEW_COMMENT, _ => this.handleNewComment());
listen(Shortcut.SAVE_COMMENT, _ => {});
@@ -355,7 +316,9 @@
listen(Shortcut.OPEN_REPLY_DIALOG, _ => this.handleOpenReplyDialog());
listen(Shortcut.TOGGLE_LEFT_PANE, _ => this.handleToggleLeftPane());
listen(Shortcut.OPEN_DOWNLOAD_DIALOG, _ => this.handleOpenDownloadDialog());
- listen(Shortcut.UP_TO_CHANGE, _ => this.handleUpToChange());
+ listen(Shortcut.UP_TO_CHANGE, _ =>
+ this.getChangeModel().navigateToChange()
+ );
listen(Shortcut.OPEN_DIFF_PREFS, _ => this.handleCommaKey());
listen(Shortcut.TOGGLE_DIFF_MODE, _ => this.handleToggleDiffMode());
listen(Shortcut.TOGGLE_FILE_REVIEWED, e => {
@@ -438,6 +401,11 @@
);
subscribe(
this,
+ () => this.getChangeModel().latestPatchNum$,
+ latestPatchNum => (this.latestPatchNum = latestPatchNum)
+ );
+ subscribe(
+ this,
() => this.getChangeModel().reviewedFiles$,
reviewedFiles => {
this.reviewedFiles = new Set(reviewedFiles) ?? new Set();
@@ -445,45 +413,82 @@
);
subscribe(
this,
- () => this.getChangeModel().diffPath$,
+ () => this.getViewModel().changeNum$,
+ changeNum => {
+ if (!changeNum || this.changeNum === changeNum) return;
+
+ // We are only setting the changeNum of the diff view once!
+ // Everything in the diff view is tied to the change. It seems better to
+ // force the re-creation of the diff view when the change number changes.
+ if (!this.changeNum) {
+ this.changeNum = changeNum;
+ } else {
+ fireEvent(this, EventType.RECREATE_DIFF_VIEW);
+ }
+ }
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().childView$,
+ childView => (this.isActiveChildView = childView === ChangeChildView.DIFF)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().diffPath$,
path => (this.path = path)
);
-
+ subscribe(
+ this,
+ () => this.getViewModel().diffLine$,
+ line => (this.focusLineNum = line)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().diffLeftSide$,
+ leftSide => (this.leftSide = leftSide)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().patchNum$,
+ patchNum => (this.patchNum = patchNum)
+ );
+ subscribe(
+ this,
+ () => this.getViewModel().basePatchNum$,
+ basePatchNum => (this.basePatchNum = basePatchNum ?? PARENT)
+ );
subscribe(
this,
() =>
combineLatest([
- this.getChangeModel().diffPath$,
+ this.getViewModel().diffPath$,
this.getChangeModel().reviewedFiles$,
]),
([path, files]) => {
- this.updateComplete.then(() => {
- assertIsDefined(this.reviewed, 'reviewed');
- this.reviewed.checked = !!path && !!files && files.includes(path);
- });
+ this.reviewed = !!path && !!files && files.includes(path);
}
);
- // When user initially loads the diff view, we want to autmatically mark
+ // When user initially loads the diff view, we want to automatically mark
// the file as reviewed if they have it enabled. We can't observe these
// properties since the method will be called anytime a property updates
// but we only want to call this on the initial load.
subscribe(
this,
() =>
- this.getChangeModel().diffPath$.pipe(
+ this.getViewModel().diffPath$.pipe(
filter(diffPath => !!diffPath),
switchMap(() =>
combineLatest([
this.getChangeModel().patchNum$,
- this.getRouterModel().routerView$,
+ this.getViewModel().childView$,
this.getUserModel().diffPreferences$,
this.getChangeModel().reviewedFiles$,
]).pipe(
filter(
- ([patchNum, routerView, diffPrefs, reviewedFiles]) =>
+ ([patchNum, childView, diffPrefs, reviewedFiles]) =>
!!patchNum &&
- routerView === GerritView.DIFF &&
+ childView === ChangeChildView.DIFF &&
!!diffPrefs &&
!!reviewedFiles
),
@@ -492,14 +497,11 @@
)
),
([patchNum, _routerView, diffPrefs]) => {
- this.setReviewedStatus(patchNum!, diffPrefs);
+ // `patchNum` must be defined, because of the `!!patchNum` filter above.
+ assertIsDefined(patchNum, 'patchNum');
+ this.setReviewedStatus(patchNum, diffPrefs);
}
);
- subscribe(
- this,
- () => this.getChangeModel().diffPath$,
- path => (this.path = path)
- );
}
static override get styles() {
@@ -692,7 +694,6 @@
override connectedCallback() {
super.connectedCallback();
- this.connected$.next(true);
this.throttledToggleFileReviewed = throttleWrap(_ =>
this.handleToggleFileReviewed()
);
@@ -703,38 +704,11 @@
override disconnectedCallback() {
this.cursor?.dispose();
- this.connected$.next(false);
super.disconnectedCallback();
}
- protected override willUpdate(changedProperties: PropertyValues) {
- super.willUpdate(changedProperties);
- if (changedProperties.has('change')) {
- this.allPatchSets = computeAllPatchSets(this.change);
- }
- if (
- changedProperties.has('commentMap') ||
- changedProperties.has('files') ||
- changedProperties.has('path')
- ) {
- this.commentSkips = this.computeCommentSkips(
- this.commentMap,
- this.files?.sortedFileList,
- this.path
- );
- }
-
- if (
- changedProperties.has('changeNum') ||
- changedProperties.has('changeComments') ||
- changedProperties.has('patchRange')
- ) {
- this.fetchFiles();
- }
- }
-
private reInitCursor() {
- assertIsDefined(this.diffHost, 'diffHost');
+ if (!this.diffHost) return;
this.cursor?.replaceDiffs([this.diffHost]);
this.cursor?.reInitCursor();
}
@@ -742,16 +716,35 @@
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
+ changedProperties.has('change') ||
+ changedProperties.has('path') ||
+ changedProperties.has('patchNum') ||
+ changedProperties.has('basePatchNum')
+ ) {
+ this.reloadDiff();
+ } else if (
+ changedProperties.has('isActiveChildView') &&
+ this.isActiveChildView
+ ) {
+ this.initializePositions();
+ }
+ if (
+ changedProperties.has('focusLineNum') ||
+ changedProperties.has('leftSide')
+ ) {
+ this.initCursor();
+ }
+ if (
+ changedProperties.has('change') ||
changedProperties.has('changeComments') ||
changedProperties.has('path') ||
- changedProperties.has('patchRange') ||
+ changedProperties.has('patchNum') ||
+ changedProperties.has('basePatchNum') ||
changedProperties.has('files')
) {
- if (this.changeComments && this.path && this.patchRange) {
+ if (this.change && this.changeComments && this.path && this.patchRange) {
assertIsDefined(this.diffHost, 'diffHost');
- const file = this.files?.changeFilesByPath
- ? this.files.changeFilesByPath[this.path]
- : undefined;
+ const file = this.files?.changeFilesByPath?.[this.path];
this.diffHost.updateComplete.then(() => {
assertIsDefined(this.path);
assertIsDefined(this.patchRange);
@@ -767,19 +760,21 @@
}
override render() {
+ if (!this.isActiveChildView) return nothing;
+ if (!this.patchNum || !this.changeNum || !this.change || !this.path) {
+ return html`<div class="loading">Loading...</div>`;
+ }
const file = this.getFileRange();
return html`
${this.renderStickyHeader()}
- <div class="loading" ?hidden=${!this.loading}>Loading...</div>
<h2 class="assistive-tech-only">Diff view</h2>
<gr-diff-host
id="diffHost"
- ?hidden=${this.loading}
.changeNum=${this.changeNum}
.change=${this.change}
- .commitRange=${this.commitRange}
.patchRange=${this.patchRange}
.file=${file}
+ .lineOfInterest=${this.getLineOfInterest()}
.path=${this.path}
.projectName=${this.change?.project}
@is-blame-loaded-changed=${this.onIsBlameLoadedChanged}
@@ -798,7 +793,7 @@
private renderStickyHeader() {
return html` <div
- class="stickyHeader ${this.computeEditMode() ? 'editMode' : ''}"
+ class="stickyHeader ${this.patchNum === EDIT ? 'editMode' : ''}"
>
<h1 class="assistive-tech-only">
Diff of ${this.path ? computeTruncatedPath(this.path) : ''}
@@ -824,7 +819,8 @@
const fileNum = this.computeFileNum(formattedFiles);
const fileNumClass = this.computeFileNumClass(fileNum, formattedFiles);
return html` <div>
- <a href=${this.getChangePath()}>${this.changeNum}</a
+ <a href=${ifDefined(this.getChangeModel().changeUrl())}
+ >${this.changeNum}</a
><span class="changeNumberColon">:</span>
<span class="headerSubject">${this.change?.subject}</span>
<input
@@ -834,6 +830,7 @@
?hidden=${!this.loggedIn}
title="Toggle reviewed status of file"
aria-label="file reviewed"
+ .checked=${this.reviewed}
@change=${this.handleReviewedChange}
/>
<div class="jumpToFileContainer">
@@ -867,7 +864,7 @@
Shortcut.UP_TO_CHANGE,
ShortcutSection.NAVIGATION
)}
- href=${this.getChangePath()}
+ href=${ifDefined(this.getChangeModel().changeUrl())}
>Up</a
>
<span class="separator"></span>
@@ -884,19 +881,10 @@
}
private renderPatchRangeLeft() {
- const revisionInfo = this.change
- ? new RevisionInfoObj(this.change)
- : undefined;
return html` <div class="patchRangeLeft">
<gr-patch-range-select
id="rangeSelect"
- .changeNum=${this.changeNum}
- .patchNum=${this.patchRange?.patchNum}
- .basePatchNum=${this.patchRange?.basePatchNum}
.filesWeblinks=${this.filesWeblinks}
- .availablePatches=${this.allPatchSets}
- .revisions=${this.change?.revisions}
- .revisionInfo=${revisionInfo}
@patch-range-change=${this.handlePatchChange}
>
</gr-patch-range-select>
@@ -1023,7 +1011,7 @@
<gr-download-dialog
id="downloadDialog"
.change=${this.change}
- .patchNum=${this.patchRange?.patchNum}
+ .patchNum=${this.patchNum}
.config=${this.serverConfig?.download}
@close=${this.handleDownloadDialogClose}
></gr-download-dialog>
@@ -1048,36 +1036,12 @@
if (!this.files || !this.path) return;
const fileInfo = this.files.changeFilesByPath[this.path];
const fileRange: FileRange = {path: this.path};
- if (fileInfo && fileInfo.old_path) {
+ if (fileInfo?.old_path) {
fileRange.basePath = fileInfo.old_path;
}
return fileRange;
}
- // Private but used in tests.
- fetchFiles() {
- if (!this.changeNum || !this.patchRange || !this.changeComments) {
- return Promise.resolve();
- }
-
- if (!this.patchRange.patchNum) {
- return Promise.resolve();
- }
-
- return this.restApiService
- .getChangeFiles(this.changeNum, this.patchRange)
- .then(changeFiles => {
- if (!changeFiles) return;
- const commentedPaths = this.changeComments!.getPaths(this.patchRange);
- const files = {...changeFiles};
- addUnmodifiedFiles(files, commentedPaths);
- this.files = {
- sortedFileList: Object.keys(files).sort(specialFilePathCompare),
- changeFilesByPath: files,
- };
- });
- }
-
private handleReviewedChange(e: Event) {
const input = e.target as HTMLInputElement;
this.setReviewed(input.checked ?? false);
@@ -1086,12 +1050,14 @@
// Private but used in tests.
setReviewed(
reviewed: boolean,
- patchNum: RevisionPatchSetNum | undefined = this.patchRange?.patchNum
+ patchNum: RevisionPatchSetNum | undefined = this.patchNum
) {
- if (this.computeEditMode()) return;
+ if (this.patchNum === EDIT) return;
if (!patchNum || !this.path || !this.changeNum) return;
// if file is already reviewed then do not make a saveReview request
if (this.reviewedFiles.has(this.path) && reviewed) return;
+ // optimistic update
+ this.reviewed = reviewed;
this.getChangeModel().setReviewedFilesStatus(
this.changeNum,
patchNum,
@@ -1102,8 +1068,7 @@
// Private but used in tests.
handleToggleFileReviewed() {
- assertIsDefined(this.reviewed);
- this.setReviewed(!this.reviewed.checked);
+ this.setReviewed(!this.reviewed);
}
private handlePrevLine() {
@@ -1148,48 +1113,13 @@
}
// Private but used in tests.
- moveToPreviousFileWithComment() {
- if (!this.commentSkips) return;
- if (!this.change) return;
- if (!this.patchRange?.patchNum) return;
-
- // If there is no previous diff with comments, then return to the change
- // view.
- if (!this.commentSkips.previous) {
- this.navToChangeView();
- return;
+ moveToFileWithComment(direction: -1 | 1) {
+ const path = this.findFileWithComment(direction);
+ if (!path) {
+ this.getChangeModel().navigateToChange();
+ } else {
+ this.getChangeModel().navigateToDiff({path});
}
-
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.commentSkips.previous,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- })
- );
- }
-
- // Private but used in tests.
- moveToNextFileWithComment() {
- if (!this.commentSkips) return;
- if (!this.change) return;
- if (!this.patchRange?.patchNum) return;
-
- // If there is no next diff with comments, then return to the change view.
- if (!this.commentSkips.next) {
- this.navToChangeView();
- return;
- }
-
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.commentSkips.next,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- })
- );
}
private handleNewComment() {
@@ -1199,14 +1129,14 @@
private handlePrevFile() {
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
- this.navToFile(this.files.sortedFileList, -1);
+ if (!this.files?.sortedPaths) return;
+ this.navToFile(this.files.sortedPaths, -1);
}
private handleNextFile() {
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
- this.navToFile(this.files.sortedFileList, 1);
+ if (!this.files?.sortedPaths) return;
+ this.navToFile(this.files.sortedPaths, 1);
}
private handleNextChunk() {
@@ -1250,11 +1180,11 @@
private navigateToUnreviewedFile(direction: string) {
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
+ if (!this.files?.sortedPaths) return;
if (!this.reviewedFiles) return;
// Ensure that the currently viewed file always appears in unreviewedFiles
// so we resolve the right "next" file.
- const unreviewedFiles = this.files.sortedFileList.filter(
+ const unreviewedFiles = this.files.sortedPaths.filter(
file => file === this.path || !this.reviewedFiles.has(file)
);
@@ -1278,7 +1208,7 @@
fireEvent(this, 'show-auth-required');
return;
}
- this.navToChangeView(true);
+ this.getChangeModel().navigateToChange(true);
}
private handleToggleLeftPane() {
@@ -1314,10 +1244,6 @@
this.downloadModal.close();
}
- private handleUpToChange() {
- this.navToChangeView();
- }
-
private handleCommaKey() {
if (!this.loggedIn) return;
assertIsDefined(this.diffPreferencesDialog, 'diffPreferencesDialog');
@@ -1337,19 +1263,6 @@
}
// Private but used in tests.
- navToChangeView(openReplyDialog = false) {
- if (!this.changeNum || !this.patchRange?.patchNum) {
- return;
- }
- this.navigateToChange(
- this.change,
- this.patchRange,
- this.change && this.change.revisions,
- openReplyDialog
- );
- }
-
- // Private but used in tests.
navToFile(
fileList: string[],
direction: -1 | 1,
@@ -1357,15 +1270,10 @@
) {
const newPath = this.getNavLinkPath(fileList, direction);
if (!newPath) return;
- if (!this.change) return;
if (!this.patchRange) return;
if (newPath.up) {
- this.navigateToChange(
- this.change,
- this.patchRange,
- this.change && this.change.revisions
- );
+ this.getChangeModel().navigateToChange();
return;
}
@@ -1376,15 +1284,7 @@
newPath.path,
this.patchRange
)?.[0].line;
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: newPath.path,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- lineNum,
- })
- );
+ this.getChangeModel().navigateToDiff({path: newPath.path, lineNum});
}
/**
@@ -1395,35 +1295,25 @@
private computeNavLinkURL(direction?: -1 | 1) {
if (!this.change) return;
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
+ if (!this.files?.sortedPaths) return;
if (!direction) return;
- const newPath = this.getNavLinkPath(this.files.sortedFileList, direction);
- if (!newPath) {
- return;
- }
-
- if (newPath.up) {
- return this.getChangePath();
- }
- return this.getDiffUrl(this.change, this.patchRange, newPath.path);
+ const newPath = this.getNavLinkPath(this.files.sortedPaths, direction);
+ if (!newPath) return;
+ if (newPath.up) return this.getChangeModel().changeUrl();
+ if (!newPath.path) return;
+ return this.getChangeModel().diffUrl({path: newPath.path});
}
private goToEditFile() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ assertIsDefined(this.path, 'path');
// TODO(taoalpha): add a shortcut for editing
const cursorAddress = this.cursor?.getAddress();
- const editUrl = createEditUrl({
- changeNum: this.change._number,
- repo: this.change.project,
+ this.getChangeModel().navigateToEdit({
path: this.path,
- patchNum: this.patchRange.patchNum,
lineNum: cursorAddress?.number,
});
- this.getNavigation().setUrl(editUrl);
}
/**
@@ -1445,7 +1335,6 @@
if (!this.path || !fileList || fileList.length === 0) {
return null;
}
-
let idx = fileList.indexOf(this.path);
if (idx === -1) {
const file = direction > 0 ? fileList[0] : fileList[fileList.length - 1];
@@ -1462,326 +1351,68 @@
return {path: fileList[idx]};
}
- // Private but used in tests.
- initLineOfInterestAndCursor(leftSide: boolean) {
- assertIsDefined(this.diffHost, 'diffHost');
- this.diffHost.lineOfInterest = this.getLineOfInterest(leftSide);
- this.initCursor(leftSide);
- }
-
- // Private but used in tests.
- displayDiffBaseAgainstLeftToast() {
- if (!this.patchRange) return;
- fireAlert(
- this,
- `Patchset ${this.patchRange.basePatchNum} vs ` +
- `${this.patchRange.patchNum} selected. Press v + \u2190 to view ` +
- `Base vs ${this.patchRange.basePatchNum}`
- );
- }
-
- private displayDiffAgainstLatestToast(latestPatchNum?: PatchSetNum) {
- if (!this.patchRange) return;
- const leftPatchset =
- this.patchRange.basePatchNum === PARENT
- ? 'Base'
- : `Patchset ${this.patchRange.basePatchNum}`;
- fireAlert(
- this,
- `${leftPatchset} vs
- ${this.patchRange.patchNum} selected\n. Press v + \u2191 to view
- ${leftPatchset} vs Patchset ${latestPatchNum}`
- );
- }
-
- private displayToasts() {
- if (!this.patchRange) return;
- if (this.patchRange.basePatchNum !== PARENT) {
- this.displayDiffBaseAgainstLeftToast();
- return;
- }
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (this.patchRange.patchNum !== latestPatchNum) {
- this.displayDiffAgainstLatestToast(latestPatchNum);
- return;
- }
- }
-
- private initCommitRange() {
- let commit: CommitId | undefined;
- let baseCommit: CommitId | undefined;
- if (!this.change) return;
- if (!this.patchRange || !this.patchRange.patchNum) return;
- const revisions = this.change.revisions ?? {};
- for (const [commitSha, revision] of Object.entries(revisions)) {
- const patchNum = revision._number;
- if (patchNum === this.patchRange.patchNum) {
- commit = commitSha as CommitId;
- const commitObj = revision.commit;
- const parents = commitObj?.parents || [];
- if (this.patchRange.basePatchNum === PARENT && parents.length) {
- baseCommit = parents[parents.length - 1].commit;
- }
- } else if (patchNum === this.patchRange.basePatchNum) {
- baseCommit = commitSha as CommitId;
- }
- }
- this.commitRange = commit && baseCommit ? {commit, baseCommit} : undefined;
- }
-
private updateUrlToDiffUrl(lineNum?: number, leftSide?: boolean) {
if (!this.change) return;
- if (!this.patchRange) return;
+ if (!this.patchNum) return;
if (!this.changeNum) return;
if (!this.path) return;
const url = createDiffUrl({
changeNum: this.changeNum,
repo: this.change.project,
- path: this.path,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- lineNum,
- leftSide,
+ patchNum: this.patchNum,
+ basePatchNum: this.basePatchNum,
+ diffView: {
+ path: this.path,
+ lineNum,
+ leftSide,
+ },
});
history.replaceState(null, '', url);
}
- // Private but used in tests.
- initPatchRange() {
- let leftSide = false;
- if (!this.change) return;
- if (this.viewState?.view !== GerritView.DIFF) return;
- if (this.viewState?.commentId) {
- const comment = this.changeComments?.findCommentById(
- this.viewState.commentId
- );
- if (!comment) {
- fireAlert(this, 'comment not found');
- this.getNavigation().setUrl(createChangeUrl({change: this.change}));
- return;
- }
- this.getChangeModel().updatePath(comment.path);
-
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (!latestPatchNum) throw new Error('Missing allPatchSets');
- this.patchRange = getPatchRangeForCommentUrl(comment, latestPatchNum);
- leftSide = isInBaseOfPatchRange(comment, this.patchRange);
-
- this.focusLineNum = comment.line;
- } else {
- if (this.viewState.path) {
- this.getChangeModel().updatePath(this.viewState.path);
- }
- if (this.viewState.patchNum) {
- this.patchRange = {
- patchNum: this.viewState.patchNum,
- basePatchNum: this.viewState.basePatchNum || PARENT,
- };
- }
- if (this.viewState.lineNum) {
- this.focusLineNum = this.viewState.lineNum;
- leftSide = !!this.viewState.leftSide;
- }
- }
- assertIsDefined(this.patchRange, 'patchRange');
- this.initLineOfInterestAndCursor(leftSide);
-
- if (this.viewState?.commentId) {
- // url is of type /comment/{commentId} which isn't meaningful
- this.updateUrlToDiffUrl(this.focusLineNum, leftSide);
- }
-
- this.commentMap = this.getPaths();
+ async reloadDiff() {
+ if (!this.diffHost) return;
+ await this.diffHost.reload(true);
+ this.reporting.diffViewDisplayed();
+ if (this.isBlameLoaded) this.loadBlame();
}
- // Private but used in tests.
- isFileUnchanged(diff?: DiffInfo) {
- if (!diff || !diff.content) return false;
- return !diff.content.some(
- content =>
- (content.a && !content.common) || (content.b && !content.common)
- );
- }
-
- private isSameDiffLoaded(value: DiffViewState) {
- return (
- this.patchRange?.basePatchNum === value.basePatchNum &&
- this.patchRange?.patchNum === value.patchNum &&
- this.path === value.path
- );
- }
-
- private async untilModelLoaded() {
- // NOTE: Wait until this page is connected before determining whether the
- // model is loaded. This can happen when params are changed when setting up
- // this view. It's unclear whether this issue is related to Polymer
- // specifically.
- if (!this.isConnected) {
- await until(this.connected$, connected => connected);
- }
- await until(
- this.getChangeModel().changeLoadingStatus$,
- status => status === LoadingStatus.LOADED
- );
- }
-
- // Private but used in tests.
- viewStateChanged() {
- if (this.viewState === undefined) return;
- const viewState = this.viewState;
-
+ /**
+ * (Re-initialize) the diff view without actually reloading the diff. The
+ * typical user journey is that the user comes back from the change page.
+ */
+ initializePositions() {
// The diff view is kept in the background once created. If the user
// scrolls in the change page, the scrolling is reflected in the diff view
// as well, which means the diff is scrolled to a random position based
// on how much the change view was scrolled.
// Hence, reset the scroll position here.
document.documentElement.scrollTop = 0;
-
- // Everything in the diff view is tied to the change. It seems better to
- // force the re-creation of the diff view when the change number changes.
- const changeChanged = this.changeNum !== viewState.changeNum;
- if (this.changeNum !== undefined && changeChanged) {
- fireEvent(this, EventType.RECREATE_DIFF_VIEW);
- return;
- } else if (
- this.changeNum !== undefined &&
- this.isSameDiffLoaded(viewState)
- ) {
- // changeNum has not changed, so check if there are changes in patchRange
- // path. If no changes then we can simply render the view as is.
- this.reporting.reportInteraction('diff-view-re-rendered');
- // Make sure to re-initialize the cursor because this is typically
- // done on the 'render' event which doesn't fire in this path as
- // rerendering is avoided.
- this.reInitCursor();
- this.diffHost?.initLayers();
- return;
- }
-
- this.files = {sortedFileList: [], changeFilesByPath: {}};
- if (this.isConnected) {
- this.getChangeModel().updatePath(undefined);
- }
- this.patchRange = undefined;
- this.commitRange = undefined;
- this.focusLineNum = undefined;
-
- if (viewState.changeNum && viewState.repo) {
- this.restApiService.setInProjectLookup(
- viewState.changeNum,
- viewState.repo
- );
- }
-
- this.changeNum = viewState.changeNum;
+ this.reInitCursor();
+ this.diffHost?.initLayers();
this.classList.remove('hideComments');
-
- // When navigating away from the page, there is a possibility that the
- // patch number is no longer a part of the URL (say when navigating to
- // the top-level change info view) and therefore undefined in `params`.
- // If route is of type /comment/<commentId>/ then no patchNum is present
- if (!viewState.patchNum && !viewState.commentLink) {
- this.reporting.error(
- 'GrDiffView',
- new Error(`Invalid diff view URL, no patchNum found: ${this.viewState}`)
- );
- return;
- }
-
- const promises: Promise<unknown>[] = [];
- if (!this.change) {
- promises.push(this.untilModelLoaded());
- }
- promises.push(this.waitUntilCommentsLoaded());
-
- if (this.diffHost) {
- this.diffHost.cancel();
- this.diffHost.clearDiffContent();
- }
- this.loading = true;
- return Promise.all(promises)
- .then(() => {
- this.loading = false;
- this.initPatchRange();
- this.initCommitRange();
- return this.updateComplete.then(() => this.diffHost!.reload(true));
- })
- .then(() => {
- this.reporting.diffViewDisplayed();
- })
- .then(() => {
- const fileUnchanged = this.isFileUnchanged(this.diff);
- if (fileUnchanged && viewState.commentLink) {
- assertIsDefined(this.change, 'change');
- assertIsDefined(this.path, 'path');
- assertIsDefined(this.patchRange, 'patchRange');
-
- if (this.patchRange.basePatchNum === PARENT) {
- // file is unchanged between Base vs X
- // hence should not show diff between Base vs Base
- return;
- }
-
- fireAlert(
- this,
- `File is unchanged between Patchset
- ${this.patchRange.basePatchNum} and
- ${this.patchRange.patchNum}. Showing diff of Base vs
- ${this.patchRange.basePatchNum}`
- );
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: this.patchRange.basePatchNum as RevisionPatchSetNum,
- basePatchNum: PARENT,
- lineNum: this.focusLineNum,
- })
- );
- return;
- }
- if (viewState.commentLink) {
- this.displayToasts();
- }
- // If the blame was loaded for a previous file and user navigates to
- // another file, then we load the blame for this file too
- if (this.isBlameLoaded) this.loadBlame();
- });
- }
-
- private async waitUntilCommentsLoaded() {
- await until(this.connected$, c => c);
- await until(this.getCommentsModel().commentsLoading$, isFalse);
}
/**
* If the params specify a diff address then configure the diff cursor.
* Private but used in tests.
*/
- initCursor(leftSide: boolean) {
- if (this.focusLineNum === undefined) {
- return;
- }
+ initCursor() {
+ if (!this.focusLineNum) return;
if (!this.cursor) return;
- if (leftSide) {
- this.cursor.side = Side.LEFT;
- } else {
- this.cursor.side = Side.RIGHT;
- }
+ this.cursor.side = this.leftSide ? Side.LEFT : Side.RIGHT;
this.cursor.initialLineNumber = this.focusLineNum;
}
// Private but used in tests.
- getLineOfInterest(leftSide: boolean): DisplayLine | undefined {
+ getLineOfInterest(): DisplayLine | undefined {
// If there is a line number specified, pass it along to the diff so that
// it will not get collapsed.
- if (!this.focusLineNum) {
- return undefined;
- }
+ if (!this.focusLineNum) return undefined;
return {
lineNum: this.focusLineNum,
- side: leftSide ? Side.LEFT : Side.RIGHT,
+ side: this.leftSide ? Side.LEFT : Side.RIGHT,
};
}
@@ -1791,83 +1422,6 @@
}
}
- private getDiffUrl(
- change?: ChangeInfo | ParsedChangeInfo,
- patchRange?: PatchRange,
- path?: string
- ) {
- if (!change || !patchRange || !path) return '';
- return createDiffUrl({
- changeNum: change._number,
- repo: change.project,
- path,
- patchNum: patchRange.patchNum,
- basePatchNum: patchRange.basePatchNum,
- });
- }
-
- /**
- * When the latest patch of the change is selected (and there is no base
- * patch) then the patch range need not appear in the URL. Return a patch
- * range object with undefined values when a range is not needed.
- */
- private getChangeUrlRange(
- patchRange?: PatchRange,
- revisions?: {[revisionId: string]: RevisionInfo | EditRevisionInfo}
- ) {
- let patchNum = undefined;
- let basePatchNum = undefined;
- let latestPatchNum = -1;
- for (const rev of Object.values(revisions || {})) {
- if (typeof rev._number === 'number') {
- latestPatchNum = Math.max(latestPatchNum, rev._number);
- }
- }
- if (!patchRange) return {patchNum, basePatchNum};
- if (
- patchRange.basePatchNum !== PARENT ||
- patchRange.patchNum !== latestPatchNum
- ) {
- patchNum = patchRange.patchNum;
- basePatchNum = patchRange.basePatchNum;
- }
- return {patchNum, basePatchNum};
- }
-
- private getChangePath() {
- if (!this.change) return '';
- if (!this.patchRange) return '';
-
- const range = this.getChangeUrlRange(
- this.patchRange,
- this.change.revisions
- );
- return createChangeUrl({
- change: this.change,
- patchNum: range.patchNum,
- basePatchNum: range.basePatchNum,
- });
- }
-
- // Private but used in tests.
- navigateToChange(
- change?: ChangeInfo | ParsedChangeInfo,
- patchRange?: PatchRange,
- revisions?: {[revisionId: string]: RevisionInfo | EditRevisionInfo},
- openReplyDialog?: boolean
- ) {
- if (!change) return;
- const range = this.getChangeUrlRange(patchRange, revisions);
- this.getNavigation().setUrl(
- createChangeUrl({
- change,
- patchNum: range.patchNum,
- basePatchNum: range.basePatchNum,
- openReplyDialog: !!openReplyDialog,
- })
- );
- }
-
// Private but used in tests
formatFilesForDropdown(): DropdownItem[] {
if (!this.files) return [];
@@ -1875,7 +1429,8 @@
if (!this.changeComments) return [];
const dropdownContent: DropdownItem[] = [];
- for (const path of this.files.sortedFileList) {
+ for (const path of this.files.sortedPaths) {
+ const file = this.files.changeFilesByPath[path];
dropdownContent.push({
text: computeDisplayPath(path),
mobileText: computeTruncatedPath(path),
@@ -1883,56 +1438,35 @@
bottomText: this.changeComments.computeCommentsString(
this.patchRange,
path,
- this.files.changeFilesByPath[path],
+ file,
/* includeUnmodified= */ true
),
- file: {...this.files.changeFilesByPath[path], __path: path},
+ file,
});
}
return dropdownContent;
}
// Private but used in tests.
- handleFileChange(e: CustomEvent) {
- if (!this.change) return;
- if (!this.patchRange) return;
-
- // This is when it gets set initially.
- const path = e.detail.value;
- if (path === this.path) {
- return;
- }
-
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path,
- patchNum: this.patchRange.patchNum,
- basePatchNum: this.patchRange.basePatchNum,
- })
- );
+ handleFileChange(e: ValueChangedEvent<string>) {
+ const path: string = e.detail.value;
+ if (path === this.path) return;
+ this.getChangeModel().navigateToDiff({path});
}
// Private but used in tests.
handlePatchChange(e: CustomEvent) {
- if (!this.change) return;
if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.patchNum) return;
const {basePatchNum, patchNum} = e.detail;
- if (
- basePatchNum === this.patchRange.basePatchNum &&
- patchNum === this.patchRange.patchNum
- ) {
+ if (basePatchNum === this.basePatchNum && patchNum === this.patchNum) {
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum,
- basePatchNum,
- })
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ patchNum,
+ basePatchNum
);
}
@@ -1957,7 +1491,7 @@
computeDownloadDropdownLinks() {
if (!this.change?.project) return [];
if (!this.changeNum) return [];
- if (!this.patchRange?.patchNum) return [];
+ if (!this.patchRange) return [];
if (!this.path) return [];
const links = [
@@ -2005,6 +1539,7 @@
return links;
}
+ // TODO: Move to view-model or router.
// Private but used in tests.
computeDownloadFileLink(
repo: RepoName,
@@ -2033,6 +1568,7 @@
return url;
}
+ // TODO: Move to view-model or router.
// Private but used in tests.
computeDownloadPatchLink(
repo: RepoName,
@@ -2046,49 +1582,19 @@
}
// Private but used in tests.
- getPaths(): CommentMap {
- if (!this.changeComments) return {};
- return this.changeComments.getPaths(this.patchRange);
- }
+ findFileWithComment(direction: -1 | 1): string | undefined {
+ const fileList = this.files?.sortedPaths;
+ const commentMap: CommentMap =
+ this.changeComments?.getPaths(this.patchRange) ?? {};
+ if (!fileList || fileList.length === 0) return undefined;
+ if (!this.path) return undefined;
- // Private but used in tests.
- computeCommentSkips(
- commentMap?: CommentMap,
- fileList?: string[],
- path?: string
- ): CommentSkips | undefined {
- if (!commentMap) return undefined;
- if (!fileList) return undefined;
- if (!path) return undefined;
-
- const skips: CommentSkips = {previous: null, next: null};
- if (!fileList.length) {
- return skips;
+ const pathIndex = fileList.indexOf(this.path);
+ const stopIndex = direction === 1 ? fileList.length : -1;
+ for (let i = pathIndex + direction; i !== stopIndex; i += direction) {
+ if (commentMap[fileList[i]]) return fileList[i];
}
- const pathIndex = fileList.indexOf(path);
-
- // Scan backward for the previous file.
- for (let i = pathIndex - 1; i >= 0; i--) {
- if (commentMap[fileList[i]]) {
- skips.previous = fileList[i];
- break;
- }
- }
-
- // Scan forward for the next file.
- for (let i = pathIndex + 1; i < fileList.length; i++) {
- if (commentMap[fileList[i]]) {
- skips.next = fileList[i];
- break;
- }
- }
-
- return skips;
- }
-
- // Private but used in tests.
- computeEditMode() {
- return this.patchRange?.patchNum === EDIT;
+ return undefined;
}
// Private but used in tests.
@@ -2131,111 +1637,89 @@
// Private but used in tests.
handleDiffAgainstBase() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- if (this.patchRange.basePatchNum === PARENT) {
+ if (this.basePatchNum === PARENT) {
fireAlert(this, 'Base is already selected.');
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: this.patchRange.patchNum,
- })
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.patchNum,
+ PARENT
);
}
// Private but used in tests.
handleDiffBaseAgainstLeft() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- if (this.patchRange.basePatchNum === PARENT) {
+ if (this.basePatchNum === PARENT) {
fireAlert(this, 'Left is already base.');
return;
}
- const lineNum =
- this.viewState?.view === GerritView.DIFF && this.viewState?.commentLink
- ? this.focusLineNum
- : undefined;
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: this.patchRange.basePatchNum as RevisionPatchSetNum,
- basePatchNum: PARENT,
- lineNum,
- })
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.basePatchNum as RevisionPatchSetNum,
+ PARENT
);
}
// Private but used in tests.
handleDiffAgainstLatest() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (this.patchRange.patchNum === latestPatchNum) {
+ if (this.patchNum === this.latestPatchNum) {
fireAlert(this, 'Latest is already selected.');
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: latestPatchNum,
- basePatchNum: this.patchRange.basePatchNum,
- })
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.latestPatchNum,
+ this.basePatchNum
);
}
// Private but used in tests.
handleDiffRightAgainstLatest() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (this.patchRange.patchNum === latestPatchNum) {
+ if (this.patchNum === this.latestPatchNum) {
fireAlert(this, 'Right is already latest.');
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: latestPatchNum,
- basePatchNum: this.patchRange.patchNum as BasePatchSetNum,
- })
+
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.latestPatchNum,
+ this.patchNum as BasePatchSetNum
);
}
// Private but used in tests.
handleDiffBaseAgainstLatest() {
- if (!this.change) return;
- if (!this.path) return;
- if (!this.patchRange) return;
+ if (!this.isActiveChildView) return;
+ assertIsDefined(this.path, 'path');
+ assertIsDefined(this.patchNum, 'patchNum');
- const latestPatchNum = computeLatestPatchNum(this.allPatchSets);
- if (
- this.patchRange.patchNum === latestPatchNum &&
- this.patchRange.basePatchNum === PARENT
- ) {
+ if (this.patchNum === this.latestPatchNum && this.basePatchNum === PARENT) {
fireAlert(this, 'Already diffing base against latest.');
return;
}
- this.getNavigation().setUrl(
- createDiffUrl({
- change: this.change,
- path: this.path,
- patchNum: latestPatchNum,
- })
+
+ this.getChangeModel().navigateToDiff(
+ {path: this.path},
+ this.latestPatchNum,
+ PARENT
);
}
@@ -2266,13 +1750,13 @@
private navigateToNextFileWithCommentThread() {
if (!this.path) return;
- if (!this.files?.sortedFileList) return;
- if (!this.patchRange) return;
+ if (!this.files?.sortedPaths) return;
+ const range = this.patchRange;
+ if (!range) return;
if (!this.change) return;
const hasComment = (path: string) =>
- this.changeComments?.getCommentsForPath(path, this.patchRange!)?.length ??
- 0 > 0;
- const filesWithComments = this.files.sortedFileList.filter(
+ this.changeComments?.getCommentsForPath(path, range)?.length ?? 0 > 0;
+ const filesWithComments = this.files.sortedPaths.filter(
file => file === this.path || hasComment(file)
);
this.navToFile(filesWithComments, 1, true);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index 6a565eb..889e9dd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -5,7 +5,6 @@
*/
import '../../../test/common-test-setup';
import './gr-diff-view';
-import {navigationToken} from '../../core/gr-navigation/gr-navigation';
import {
ChangeStatus,
DiffViewMode,
@@ -18,38 +17,29 @@
query,
queryAll,
queryAndAssert,
- stubReporting,
stubRestApi,
waitEventLoop,
waitUntil,
} from '../../../test/test-utils';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
import {
- GerritView,
- routerModelToken,
-} from '../../../services/router/router-model';
-import {
createRevisions,
createComment as createCommentGeneric,
- TEST_NUMERIC_CHANGE_ID,
createDiff,
- createPatchRange,
createServerInfo,
createConfig,
createParsedChange,
createRevision,
- createCommit,
createFileInfo,
+ createDiffViewState,
+ TEST_NUMERIC_CHANGE_ID,
} from '../../../test/test-data-generators';
import {
BasePatchSetNum,
CommentInfo,
- CommitId,
EDIT,
- FileInfo,
NumericChangeId,
PARENT,
- PatchRange,
PatchSetNum,
PatchSetNumber,
PathToCommentsInfoMap,
@@ -58,17 +48,15 @@
UrlEncodedCommentId,
} from '../../../types/common';
import {CursorMoveResult} from '../../../api/core';
-import {DiffInfo, Side} from '../../../api/diff';
+import {Side} from '../../../api/diff';
import {Files, GrDiffView} from './gr-diff-view';
import {DropdownItem} from '../../shared/gr-dropdown-list/gr-dropdown-list';
-import {SinonFakeTimers, SinonStub, SinonSpy} from 'sinon';
+import {SinonFakeTimers, SinonStub} from 'sinon';
import {
changeModelToken,
ChangeModel,
LoadingStatus,
} from '../../../models/change/change-model';
-import {CommentMap} from '../../../utils/comment-util';
-import {ParsedChangeInfo} from '../../../types/types';
import {assertIsDefined} from '../../../utils/common-util';
import {GrDiffModeSelector} from '../../../embed/diff/gr-diff-mode-selector/gr-diff-mode-selector';
import {fixture, html, assert} from '@open-wc/testing';
@@ -85,6 +73,11 @@
BrowserModel,
browserModelToken,
} from '../../../models/browser/browser-model';
+import {
+ ChangeViewModel,
+ changeViewModelToken,
+} from '../../../models/views/change';
+import {FileNameToNormalizedFileInfoMap} from '../../../models/change/files-model';
function createComment(
id: string,
@@ -107,25 +100,27 @@
let clock: SinonFakeTimers;
let diffCommentsStub;
let getDiffRestApiStub: SinonStub;
- let setUrlStub: SinonStub;
+ let navToChangeStub: SinonStub;
+ let navToDiffStub: SinonStub;
+ let navToEditStub: SinonStub;
let changeModel: ChangeModel;
+ let viewModel: ChangeViewModel;
let commentsModel: CommentsModel;
let browserModel: BrowserModel;
let userModel: UserModel;
function getFilesFromFileList(fileList: string[]): Files {
const changeFilesByPath = fileList.reduce((files, path) => {
- files[path] = createFileInfo();
+ files[path] = createFileInfo(path);
return files;
- }, {} as {[path: string]: FileInfo});
+ }, {} as FileNameToNormalizedFileInfoMap);
return {
- sortedFileList: fileList,
+ sortedPaths: fileList,
changeFilesByPath,
};
}
setup(async () => {
- setUrlStub = sinon.stub(testResolver(navigationToken), 'setUrl');
stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
@@ -144,14 +139,17 @@
stubRestApi('getPortedComments').returns(Promise.resolve({}));
element = await fixture(html`<gr-diff-view></gr-diff-view>`);
- element.changeNum = 42 as NumericChangeId;
+ viewModel = testResolver(changeViewModelToken);
+ viewModel.setState(createDiffViewState());
+ await waitUntil(() => element.changeNum === TEST_NUMERIC_CHANGE_ID);
element.path = 'some/path.txt';
element.change = createParsedChange();
element.diff = {...createDiff(), content: []};
getDiffRestApiStub = stubRestApi('getDiff');
// Delayed in case a test updates element.diff.
getDiffRestApiStub.callsFake(() => Promise.resolve(element.diff));
- element.patchRange = createPatchRange();
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.changeComments = new ChangeComments({
'/COMMIT_MSG': [
createComment('c1', 10, 2, '/COMMIT_MSG'),
@@ -163,6 +161,9 @@
changeModel = testResolver(changeModelToken);
browserModel = testResolver(browserModelToken);
userModel = testResolver(userModelToken);
+ navToChangeStub = sinon.stub(changeModel, 'navigateToChange');
+ navToDiffStub = sinon.stub(changeModel, 'navigateToDiff');
+ navToEditStub = sinon.stub(changeModel, 'navigateToEdit');
commentsModel.setState({
comments: {},
@@ -179,279 +180,6 @@
sinon.restore();
});
- test('viewState change triggers diffViewDisplayed()', () => {
- const diffViewDisplayedStub = stubReporting('diffViewDisplayed');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- sinon.stub(element, 'initPatchRange');
- sinon.stub(element, 'fetchFiles');
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- path: '/COMMIT_MSG',
- };
- element.path = '/COMMIT_MSG';
- element.patchRange = createPatchRange();
- return viewStateChangedSpy.returnValues[0]?.then(() => {
- assert.isTrue(diffViewDisplayedStub.calledOnce);
- });
- });
-
- suite('comment route', () => {
- let initLineOfInterestAndCursorStub: SinonStub;
- let replaceStateStub: SinonStub;
- let viewStateChangedSpy: SinonSpy;
- setup(() => {
- initLineOfInterestAndCursorStub = sinon.stub(
- element,
- 'initLineOfInterestAndCursor'
- );
- replaceStateStub = sinon.stub(history, 'replaceState');
- sinon.stub(element, 'fetchFiles');
- stubReporting('diffViewDisplayed');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- changeModel.setState({
- change: {
- ...createParsedChange(),
- revisions: createRevisions(11),
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- });
-
- test('comment url resolves to comment.patch_set vs latest', () => {
- commentsModel.setState({
- comments: {
- '/COMMIT_MSG': [
- createComment('c1', 10, 2, '/COMMIT_MSG'),
- createComment('c3', 10, PARENT, '/COMMIT_MSG'),
- ],
- },
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- commentLink: true,
- commentId: 'c1' as UrlEncodedCommentId,
- path: 'abcd',
- patchNum: 1 as RevisionPatchSetNum,
- };
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(11),
- };
- return viewStateChangedSpy.returnValues[0].then(() => {
- assert.isTrue(
- initLineOfInterestAndCursorStub.calledWithExactly(true)
- );
- assert.equal(element.focusLineNum, 10);
- assert.equal(element.patchRange?.patchNum, 11 as RevisionPatchSetNum);
- assert.equal(element.patchRange?.basePatchNum, 2 as BasePatchSetNum);
- assert.isTrue(replaceStateStub.called);
- });
- });
- });
-
- test('viewState change causes blame to load if it was set to true', () => {
- // Blame loads for subsequent files if it was loaded for one file
- element.isBlameLoaded = true;
- stubReporting('diffViewDisplayed');
- const loadBlameStub = sinon.stub(element, 'loadBlame');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- sinon.stub(element, 'initPatchRange');
- sinon.stub(element, 'fetchFiles');
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- path: '/COMMIT_MSG',
- };
- element.path = '/COMMIT_MSG';
- element.patchRange = createPatchRange();
- return viewStateChangedSpy.returnValues[0]!.then(() => {
- assert.isTrue(element.isBlameLoaded);
- assert.isTrue(loadBlameStub.calledOnce);
- });
- });
-
- test('unchanged diff X vs latest from comment links navigates to base vs X', async () => {
- commentsModel.setState({
- comments: {
- '/COMMIT_MSG': [
- createComment('c1', 10, 2, '/COMMIT_MSG'),
- createComment('c3', 10, PARENT, '/COMMIT_MSG'),
- ],
- },
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
- stubReporting('diffViewDisplayed');
- sinon.stub(element, 'loadBlame');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- sinon.stub(element, 'isFileUnchanged').returns(true);
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- changeModel.setState({
- change: {
- ...createParsedChange(),
- revisions: createRevisions(11),
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- path: '/COMMIT_MSG',
- commentLink: true,
- commentId: 'c1' as UrlEncodedCommentId,
- };
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(11),
- };
- await viewStateChangedSpy.returnValues[0];
- assert.isTrue(setUrlStub.calledOnce);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/2//COMMIT_MSG#10'
- );
- });
-
- test('unchanged diff Base vs latest from comment does not navigate', async () => {
- commentsModel.setState({
- comments: {
- '/COMMIT_MSG': [
- createComment('c1', 10, 2, '/COMMIT_MSG'),
- createComment('c3', 10, PARENT, '/COMMIT_MSG'),
- ],
- },
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
- stubReporting('diffViewDisplayed');
- sinon.stub(element, 'loadBlame');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- sinon.stub(element, 'isFileUnchanged').returns(true);
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- changeModel.setState({
- change: {
- ...createParsedChange(),
- revisions: createRevisions(11),
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- path: '/COMMIT_MSG',
- commentLink: true,
- commentId: 'c3' as UrlEncodedCommentId,
- };
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(11),
- };
- await viewStateChangedSpy.returnValues[0];
- assert.isFalse(setUrlStub.calledOnce);
- });
-
- test('isFileUnchanged', () => {
- let diff: DiffInfo = {
- ...createDiff(),
- content: [
- {a: ['abcd'], ab: ['ef']},
- {b: ['ancd'], a: ['xx']},
- ],
- };
- assert.equal(element.isFileUnchanged(diff), false);
- diff = {
- ...createDiff(),
- content: [{ab: ['abcd']}, {ab: ['ancd']}],
- };
- assert.equal(element.isFileUnchanged(diff), true);
- diff = {
- ...createDiff(),
- content: [
- {a: ['abcd'], ab: ['ef'], common: true},
- {b: ['ancd'], ab: ['xx']},
- ],
- };
- assert.equal(element.isFileUnchanged(diff), false);
- diff = {
- ...createDiff(),
- content: [
- {a: ['abcd'], ab: ['ef'], common: true},
- {b: ['ancd'], ab: ['xx'], common: true},
- ],
- };
- assert.equal(element.isFileUnchanged(diff), true);
- });
-
- test('diff toast to go to latest is shown and not base', async () => {
- commentsModel.setState({
- comments: {
- '/COMMIT_MSG': [
- createComment('c1', 10, 2, '/COMMIT_MSG'),
- createComment('c3', 10, PARENT, '/COMMIT_MSG'),
- ],
- },
- robotComments: {},
- drafts: {},
- portedComments: {},
- portedDrafts: {},
- discardedDrafts: [],
- });
-
- stubReporting('diffViewDisplayed');
- sinon.stub(element, 'loadBlame');
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload').returns(Promise.resolve());
- const viewStateChangedSpy = sinon.spy(element, 'viewStateChanged');
- element.change = undefined;
- changeModel.setState({
- change: {
- ...createParsedChange(),
- revisions: createRevisions(11),
- },
- loadingStatus: LoadingStatus.LOADED,
- });
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
- sinon.stub(element, 'isFileUnchanged').returns(false);
- const toastStub = sinon.stub(element, 'displayDiffBaseAgainstLeftToast');
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- repo: 'p' as RepoName,
- commentId: 'c1' as UrlEncodedCommentId,
- commentLink: true,
- };
- await viewStateChangedSpy.returnValues[0];
- assert.isTrue(toastStub.called);
- });
-
test('toggle left diff with a hotkey', () => {
assertIsDefined(element.diffHost);
const toggleLeftDiffStub = sinon.stub(element.diffHost, 'toggleLeftDiff');
@@ -460,20 +188,17 @@
});
test('renders', async () => {
- clock = sinon.useFakeTimers();
- element.changeNum = 42 as NumericChangeId;
browserModel.setScreenWidth(0);
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
- element.change = {
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
+ const change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
},
};
+ changeModel.updateStateChange(change);
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
@@ -625,9 +350,8 @@
</a>
</div>
</div>
- <div class="loading">Loading...</div>
<h2 class="assistive-tech-only">Diff view</h2>
- <gr-diff-host hidden="" id="diffHost"> </gr-diff-host>
+ <gr-diff-host id="diffHost"> </gr-diff-host>
<gr-apply-fix-dialog id="applyFixDialog"> </gr-apply-fix-dialog>
<gr-diff-preferences-dialog id="diffPreferencesDialog">
</gr-diff-preferences-dialog>
@@ -643,10 +367,8 @@
clock = sinon.useFakeTimers();
element.changeNum = 42 as NumericChangeId;
browserModel.setScreenWidth(0);
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -662,51 +384,42 @@
element.path = 'glados.txt';
element.loggedIn = true;
await element.updateComplete;
- setUrlStub.reset();
+ navToChangeStub.reset();
pressKey(element, 'u');
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
+ assert.isTrue(navToChangeStub.calledOnce);
await element.updateComplete;
pressKey(element, ']');
- assert.equal(setUrlStub.callCount, 2);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/wheatley.md'
- );
+ assert.equal(navToDiffStub.callCount, 1);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'wheatley.md', lineNum: undefined},
+ ]);
+
element.path = 'wheatley.md';
await element.updateComplete;
- assert.isTrue(element.loading);
-
pressKey(element, '[');
- assert.equal(setUrlStub.callCount, 3);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/glados.txt'
- );
+ assert.equal(navToDiffStub.callCount, 2);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'glados.txt', lineNum: undefined},
+ ]);
+
element.path = 'glados.txt';
await element.updateComplete;
- assert.isTrue(element.loading);
-
pressKey(element, '[');
- assert.equal(setUrlStub.callCount, 4);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/chell.go'
- );
+ assert.equal(navToDiffStub.callCount, 3);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'chell.go', lineNum: undefined},
+ ]);
+
element.path = 'chell.go';
await element.updateComplete;
- assert.isTrue(element.loading);
-
pressKey(element, '[');
- assert.equal(setUrlStub.callCount, 5);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
+ assert.equal(navToChangeStub.callCount, 2);
await element.updateComplete;
- assert.isTrue(element.loading);
assertIsDefined(element.diffPreferencesDialog);
const showPrefsStub = sinon
@@ -756,12 +469,8 @@
);
assert.isFalse(element.diffHost.diffElement.displayLine);
- // Note that stubbing setReviewed means that the value of the
- // `element.reviewed` checkbox is not flipped.
const setReviewedStub = sinon.stub(element, 'setReviewed');
const handleToggleSpy = sinon.spy(element, 'handleToggleFileReviewed');
- assertIsDefined(element.reviewed);
- element.reviewed.checked = false;
assert.isFalse(handleToggleSpy.called);
assert.isFalse(setReviewedStub.called);
@@ -791,10 +500,8 @@
'wheatley.md': [createComment('c2', 21, 10, 'wheatley.md')],
};
element.changeComments = new ChangeComments(comment);
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -810,23 +517,21 @@
element.path = 'glados.txt';
element.loggedIn = true;
await element.updateComplete;
- setUrlStub.reset();
+ navToDiffStub.reset();
pressKey(element, 'N');
await element.updateComplete;
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/wheatley.md#21'
- );
+ assert.equal(navToDiffStub.callCount, 1);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'wheatley.md', lineNum: 21},
+ ]);
element.path = 'wheatley.md'; // navigated to next file
pressKey(element, 'N');
await element.updateComplete;
- assert.equal(setUrlStub.callCount, 2);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42');
+ assert.equal(navToChangeStub.callCount, 1);
});
test('shift+x shortcut toggles all diff context', async () => {
@@ -838,114 +543,61 @@
});
test('diff against base', async () => {
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = 5 as BasePatchSetNum;
await element.updateComplete;
element.handleDiffAgainstBase();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/some/path.txt'
- );
+ const expected = [{path: 'some/path.txt'}, 10, PARENT];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('diff against latest', async () => {
element.path = 'foo';
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(12),
- };
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.latestPatchNum = 12 as PatchSetNumber;
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = 5 as BasePatchSetNum;
await element.updateComplete;
element.handleDiffAgainstLatest();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..12/foo'
- );
+ const expected = [{path: 'foo'}, 12, 5];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('handleDiffBaseAgainstLeft', async () => {
element.path = 'foo';
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(10),
- };
- element.patchRange = {
+ element.latestPatchNum = 10 as PatchSetNumber;
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = 1 as BasePatchSetNum;
+ viewModel.setState({
+ ...createDiffViewState(),
patchNum: 3 as RevisionPatchSetNum,
basePatchNum: 1 as BasePatchSetNum,
- };
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 3 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- path: 'foo',
- };
+ diffView: {path: 'foo'},
+ });
await element.updateComplete;
element.handleDiffBaseAgainstLeft();
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1/foo');
- });
-
- test('handleDiffBaseAgainstLeft when initially navigating to a comment', () => {
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(10),
- };
- element.patchRange = {
- patchNum: 3 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
- sinon.stub(element, 'viewStateChanged');
- element.viewState = {
- commentLink: true,
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- };
- element.focusLineNum = 10;
- element.handleDiffBaseAgainstLeft();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1/some/path.txt#10'
- );
+ const expected = [{path: 'foo'}, 1, PARENT];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('handleDiffRightAgainstLatest', async () => {
element.path = 'foo';
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(10),
- };
- element.patchRange = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: 3 as RevisionPatchSetNum,
- };
+ element.latestPatchNum = 10 as PatchSetNumber;
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = 1 as BasePatchSetNum;
await element.updateComplete;
element.handleDiffRightAgainstLatest();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/3..10/foo'
- );
+ const expected = [{path: 'foo'}, 10, 3];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('handleDiffBaseAgainstLatest', async () => {
- element.change = {
- ...createParsedChange(),
- revisions: createRevisions(10),
- };
- element.patchRange = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: 3 as RevisionPatchSetNum,
- };
+ element.latestPatchNum = 10 as PatchSetNumber;
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = 1 as BasePatchSetNum;
await element.updateComplete;
element.handleDiffBaseAgainstLatest();
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/10/some/path.txt'
- );
+ const expected = [{path: 'some/path.txt'}, 10, PARENT];
+ assert.deepEqual(navToDiffStub.lastCall.args, expected);
});
test('A fires an error event when not logged in', async () => {
@@ -954,16 +606,14 @@
element.addEventListener('show-auth-required', loggedInErrorSpy);
pressKey(element, 'a');
await element.updateComplete;
- assert.isFalse(setUrlStub.calledOnce);
+ assert.isFalse(navToDiffStub.calledOnce);
assert.isTrue(loggedInErrorSpy.called);
});
test('A navigates to change with logged in', async () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = 5 as BasePatchSetNum;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -976,25 +626,20 @@
await element.updateComplete;
const loggedInErrorSpy = sinon.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
- setUrlStub.reset();
+ navToDiffStub.reset();
pressKey(element, 'a');
await element.updateComplete;
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..10?openReplyDialog=true'
- );
+ assert.isTrue(navToChangeStub.calledOnce);
+ assert.deepEqual(navToChangeStub.lastCall.args, [true]);
assert.isFalse(loggedInErrorSpy.called);
});
test('A navigates to change with old patch number with logged in', async () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1008,20 +653,15 @@
element.addEventListener('show-auth-required', loggedInErrorSpy);
pressKey(element, 'a');
await element.updateComplete;
- assert.isTrue(setUrlStub.calledOnce);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1?openReplyDialog=true'
- );
+ assert.isTrue(navToChangeStub.calledOnce);
+ assert.deepEqual(navToChangeStub.lastCall.args, [true]);
assert.isFalse(loggedInErrorSpy.called);
});
test('keyboard shortcuts with patch range', () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = 5 as BasePatchSetNum;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1038,40 +678,31 @@
element.path = 'glados.txt';
pressKey(element, 'u');
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
+ assert.equal(navToChangeStub.callCount, 1);
pressKey(element, ']');
- assert.isTrue(element.loading);
- assert.equal(setUrlStub.callCount, 2);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..10/wheatley.md'
- );
+ assert.equal(navToDiffStub.callCount, 1);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'wheatley.md', lineNum: undefined},
+ ]);
element.path = 'wheatley.md';
pressKey(element, '[');
- assert.isTrue(element.loading);
- assert.equal(setUrlStub.callCount, 3);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..10/glados.txt'
- );
+ assert.equal(navToDiffStub.callCount, 2);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'glados.txt', lineNum: undefined},
+ ]);
element.path = 'glados.txt';
pressKey(element, '[');
- assert.isTrue(element.loading);
- assert.equal(setUrlStub.callCount, 4);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/5..10/chell.go'
- );
+ assert.equal(navToDiffStub.callCount, 3);
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'chell.go', lineNum: undefined},
+ ]);
element.path = 'chell.go';
pressKey(element, '[');
- assert.isTrue(element.loading);
- assert.equal(setUrlStub.callCount, 5);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/5..10');
+ assert.equal(navToChangeStub.callCount, 2);
assertIsDefined(element.downloadModal);
const downloadModalStub = sinon.stub(element.downloadModal, 'showModal');
@@ -1079,12 +710,10 @@
assert.isTrue(downloadModalStub.called);
});
- test('keyboard shortcuts with old patch number', () => {
+ test('keyboard shortcuts with old patch number', async () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1101,53 +730,57 @@
element.path = 'glados.txt';
pressKey(element, 'u');
- assert.isTrue(setUrlStub.calledOnce);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
+ assert.isTrue(navToChangeStub.calledOnce);
pressKey(element, ']');
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1/wheatley.md'
- );
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'wheatley.md', lineNum: undefined},
+ ]);
element.path = 'wheatley.md';
pressKey(element, '[');
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1/glados.txt'
- );
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'glados.txt', lineNum: undefined},
+ ]);
element.path = 'glados.txt';
pressKey(element, '[');
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/test-project/+/42/1/chell.go'
- );
- element.path = 'chell.go';
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: 'chell.go', lineNum: undefined},
+ ]);
- setUrlStub.reset();
+ element.path = 'chell.go';
+ await element.updateComplete;
+ navToDiffStub.reset();
pressKey(element, '[');
- assert.isTrue(setUrlStub.calledOnce);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/1');
+ assert.equal(navToChangeStub.callCount, 2);
+ });
+
+ test('reloadDiff is called when patchNum changes', async () => {
+ const reloadStub = sinon.stub(element, 'reloadDiff');
+ element.patchNum = 5 as RevisionPatchSetNum;
+ await element.updateComplete;
+ assert.isTrue(reloadStub.called);
+ });
+
+ test('initializePositions is called when view becomes active', async () => {
+ const reloadStub = sinon.stub(element, 'reloadDiff');
+ const initializeStub = sinon.stub(element, 'initializePositions');
+
+ element.isActiveChildView = false;
+ await element.updateComplete;
+ element.isActiveChildView = true;
+ await element.updateComplete;
+
+ assert.isTrue(initializeStub.calledOnce);
+ assert.isFalse(reloadStub.called);
});
test('edit should redirect to edit page', async () => {
element.loggedIn = true;
element.path = 't.txt';
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
- element.change = {
- ...createParsedChange(),
- _number: 42 as NumericChangeId,
- project: 'gerrit' as RepoName,
- status: ChangeStatus.NEW,
- revisions: {
- a: createRevision(1),
- b: createRevision(2),
- },
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
await element.updateComplete;
const editBtn = queryAndAssert<GrButton>(
element,
@@ -1155,28 +788,18 @@
);
assert.isTrue(!!editBtn);
editBtn.click();
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(setUrlStub.lastCall.firstArg, '/c/gerrit/+/42/1/t.txt,edit');
+ assert.equal(navToEditStub.callCount, 1);
+ assert.deepEqual(navToEditStub.lastCall.args, [
+ {path: 't.txt', lineNum: undefined},
+ ]);
});
test('edit should redirect to edit page with line number', async () => {
const lineNumber = 42;
element.loggedIn = true;
element.path = 't.txt';
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
- element.change = {
- ...createParsedChange(),
- _number: 42 as NumericChangeId,
- project: 'gerrit' as RepoName,
- status: ChangeStatus.NEW,
- revisions: {
- a: createRevision(1),
- b: createRevision(2),
- },
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
assertIsDefined(element.cursor);
sinon
.stub(element.cursor, 'getAddress')
@@ -1188,11 +811,10 @@
);
assert.isTrue(!!editBtn);
editBtn.click();
- assert.equal(setUrlStub.callCount, 1);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/gerrit/+/42/1/t.txt,edit#42'
- );
+ assert.equal(navToEditStub.callCount, 1);
+ assert.deepEqual(navToEditStub.lastCall.args, [
+ {path: 't.txt', lineNum: 42},
+ ]);
});
async function isEditVisibile({
@@ -1204,10 +826,8 @@
}): Promise<boolean> {
element.loggedIn = loggedIn;
element.path = 't.txt';
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1304,16 +924,10 @@
});
suite('url parameters', () => {
- setup(() => {
- sinon.stub(element, 'fetchFiles');
- });
-
test('_formattedFiles', () => {
element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
+ element.patchNum = 10 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
@@ -1386,18 +1000,19 @@
});
test('prev/up/next links', async () => {
- element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 10 as RevisionPatchSetNum,
- };
- element.change = {
+ viewModel.setState({
+ ...createDiffViewState(),
+ });
+ const change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(10),
},
};
+ changeModel.updateStateChange(change);
+ await element.updateComplete;
+
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
@@ -1417,24 +1032,30 @@
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/10/wheatley.md'
);
+
element.path = 'wheatley.md';
await element.updateComplete;
+
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/10/glados.txt'
);
assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42');
assert.equal(linkEls[2].getAttribute('href'), '/c/test-project/+/42');
+
element.path = 'chell.go';
await element.updateComplete;
+
assert.equal(linkEls[0].getAttribute('href'), '/c/test-project/+/42');
assert.equal(linkEls[1].getAttribute('href'), '/c/test-project/+/42');
assert.equal(
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/10/glados.txt'
);
+
element.path = 'not_a_real_file';
await element.updateComplete;
+
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/10/wheatley.md'
@@ -1447,26 +1068,30 @@
});
test('prev/up/next links with patch range', async () => {
- element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
+ viewModel.setState({
+ ...createDiffViewState(),
basePatchNum: 5 as BasePatchSetNum,
patchNum: 10 as RevisionPatchSetNum,
- };
- element.change = {
+ diffView: {path: 'glados.txt'},
+ });
+ const change = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
a: createRevision(5),
b: createRevision(10),
+ c: createRevision(12),
},
};
+ changeModel.updateStateChange(change);
element.files = getFilesFromFileList([
'chell.go',
'glados.txt',
'wheatley.md',
]);
- element.path = 'glados.txt';
- await element.updateComplete;
+ await waitUntil(() => element.path === 'glados.txt');
+ await waitUntil(() => element.patchRange?.patchNum === 10);
+
const linkEls = queryAll(element, '.navLink');
assert.equal(linkEls.length, 3);
assert.equal(
@@ -1481,8 +1106,10 @@
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/5..10/wheatley.md'
);
- element.path = 'wheatley.md';
- await element.updateComplete;
+
+ viewModel.updateState({diffView: {path: 'wheatley.md'}});
+ await waitUntil(() => element.path === 'wheatley.md');
+
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/5..10/glados.txt'
@@ -1495,8 +1122,10 @@
linkEls[2].getAttribute('href'),
'/c/test-project/+/42/5..10'
);
- element.path = 'chell.go';
- await element.updateComplete;
+
+ viewModel.updateState({diffView: {path: 'chell.go'}});
+ await waitUntil(() => element.path === 'chell.go');
+
assert.equal(
linkEls[0].getAttribute('href'),
'/c/test-project/+/42/5..10'
@@ -1513,32 +1142,24 @@
});
test('handlePatchChange calls setUrl correctly', async () => {
- element.change = {
- ...createParsedChange(),
- _number: 321 as NumericChangeId,
- project: 'foo/bar' as RepoName,
- };
element.path = 'path/to/file.txt';
-
- element.patchRange = {
- basePatchNum: PARENT,
- patchNum: 3 as RevisionPatchSetNum,
- };
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
await element.updateComplete;
const detail = {
basePatchNum: PARENT,
patchNum: 1 as RevisionPatchSetNum,
};
-
queryAndAssert(element, '#rangeSelect').dispatchEvent(
new CustomEvent('patch-range-change', {detail, bubbles: false})
);
- assert.equal(
- setUrlStub.lastCall.firstArg,
- '/c/foo/bar/+/321/1/path/to/file.txt'
- );
+ assert.deepEqual(navToDiffStub.lastCall.args, [
+ {path: element.path},
+ detail.patchNum,
+ detail.basePatchNum,
+ ]);
});
test(
@@ -1559,23 +1180,13 @@
manual_review: true,
};
userModel.setDiffPreferences(diffPreferences);
+ viewModel.updateState({diffView: {path: 'wheatley.md'}});
changeModel.setState({
change: createParsedChange(),
- diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
- testResolver(routerModelToken).setState({
- changeNum: TEST_NUMERIC_CHANGE_ID,
- view: GerritView.DIFF,
- patchNum: 2 as RevisionPatchSetNum,
- });
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
-
await waitUntil(() => setReviewedStatusStub.called);
assert.isFalse(setReviewedFileStatusStub.called);
@@ -1601,55 +1212,39 @@
manual_review: false,
};
userModel.setDiffPreferences(diffPreferences);
+ viewModel.updateState({diffView: {path: 'wheatley.md'}});
changeModel.setState({
change: createParsedChange(),
- diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
- testResolver(routerModelToken).setState({
- changeNum: TEST_NUMERIC_CHANGE_ID,
- view: GerritView.DIFF,
- patchNum: 22 as RevisionPatchSetNum,
- });
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
-
await waitUntil(() => setReviewedFileStatusStub.called);
assert.isTrue(setReviewedFileStatusStub.called);
});
test('file review status', async () => {
+ const saveReviewedStub = sinon
+ .stub(changeModel, 'setReviewedFilesStatus')
+ .callsFake(() => Promise.resolve());
+ userModel.setDiffPreferences(createDefaultDiffPrefs());
+ viewModel.updateState({
+ patchNum: 1 as RevisionPatchSetNum,
+ basePatchNum: PARENT,
+ diffView: {path: '/COMMIT_MSG'},
+ });
changeModel.setState({
change: createParsedChange(),
- diffPath: '/COMMIT_MSG',
reviewedFiles: [],
loadingStatus: LoadingStatus.LOADED,
});
element.loggedIn = true;
- const saveReviewedStub = sinon
- .stub(changeModel, 'setReviewedFilesStatus')
- .callsFake(() => Promise.resolve());
+ await waitUntil(() => element.patchRange?.patchNum === 1);
+ await element.updateComplete;
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
- userModel.setDiffPreferences(createDefaultDiffPrefs());
-
- testResolver(routerModelToken).setState({
- changeNum: TEST_NUMERIC_CHANGE_ID,
- view: GerritView.DIFF,
- patchNum: 2 as RevisionPatchSetNum,
- });
-
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
-
await waitUntil(() => saveReviewedStub.called);
changeModel.updateStateFileReviewed('/COMMIT_MSG', true);
@@ -1657,13 +1252,13 @@
const reviewedStatusCheckBox = queryAndAssert<HTMLInputElement>(
element,
- 'input[type="checkbox"]'
+ 'input#reviewed'
);
assert.isTrue(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
- 2,
+ 1,
'/COMMIT_MSG',
true,
]);
@@ -1672,7 +1267,7 @@
assert.isFalse(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
- 2,
+ 1,
'/COMMIT_MSG',
false,
]);
@@ -1684,18 +1279,17 @@
assert.isTrue(reviewedStatusCheckBox.checked);
assert.deepEqual(saveReviewedStub.lastCall.args, [
42,
- 2,
+ 1,
'/COMMIT_MSG',
true,
]);
const callCount = saveReviewedStub.callCount;
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
+ viewModel.setState({
+ ...createDiffViewState(),
repo: 'test' as RepoName,
- };
+ });
await element.updateComplete;
// saveReviewedState observer observes viewState, but should not fire when
@@ -1703,36 +1297,27 @@
assert.equal(saveReviewedStub.callCount, callCount);
});
- test('file review status with edit loaded', async () => {
+ test('do not set file review status for EDIT patchset', async () => {
const saveReviewedStub = sinon.stub(
changeModel,
'setReviewedFilesStatus'
);
- element.patchRange = {
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: EDIT,
- };
+ element.patchNum = EDIT;
+ element.basePatchNum = 1 as BasePatchSetNum;
await waitEventLoop();
- assert.isTrue(element.computeEditMode());
element.setReviewed(true);
+
assert.isFalse(saveReviewedStub.called);
});
test('hash is determined from viewState', async () => {
assertIsDefined(element.diffHost);
sinon.stub(element.diffHost, 'reload');
- const initLineStub = sinon.stub(element, 'initLineOfInterestAndCursor');
+ const initLineStub = sinon.stub(element, 'initCursor');
- element.loggedIn = true;
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- path: '/COMMIT_MSG',
- };
+ element.focusLineNum = 123;
await element.updateComplete;
await waitEventLoop();
@@ -1780,115 +1365,56 @@
assert.isTrue(diffModeSelector.classList.contains('hide'));
});
- suite('commitRange', () => {
- const change: ParsedChangeInfo = {
- ...createParsedChange(),
- _number: 42 as NumericChangeId,
- revisions: {
- 'commit-sha-1': {
- ...createRevision(1),
- commit: {
- ...createCommit(),
- parents: [{subject: 's1', commit: 'sha-1-parent' as CommitId}],
- },
- },
- 'commit-sha-2': createRevision(2),
- 'commit-sha-3': createRevision(3),
- 'commit-sha-4': createRevision(4),
- 'commit-sha-5': {
- ...createRevision(5),
- commit: {
- ...createCommit(),
- parents: [{subject: 's5', commit: 'sha-5-parent' as CommitId}],
- },
- },
- },
- };
- setup(async () => {
- assertIsDefined(element.diffHost);
- sinon.stub(element.diffHost, 'reload');
- sinon.stub(element, 'initCursor');
- element.change = change;
- await element.updateComplete;
- await element.diffHost.updateComplete;
- });
-
- test('uses the patchNum and basePatchNum ', async () => {
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 4 as RevisionPatchSetNum,
- basePatchNum: 2 as BasePatchSetNum,
- path: '/COMMIT_MSG',
- };
- element.change = change;
- await element.updateComplete;
- await waitEventLoop();
- assert.deepEqual(element.commitRange, {
- baseCommit: 'commit-sha-2' as CommitId,
- commit: 'commit-sha-4' as CommitId,
- });
- });
-
- test('uses the parent when there is no base patch num ', async () => {
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 5 as RevisionPatchSetNum,
- path: '/COMMIT_MSG',
- };
- element.change = change;
- await element.updateComplete;
- await waitEventLoop();
- assert.deepEqual(element.commitRange, {
- commit: 'commit-sha-5' as CommitId,
- baseCommit: 'sha-5-parent' as CommitId,
- });
- });
- });
-
test('initCursor', () => {
assertIsDefined(element.cursor);
assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when viewState specify no cursor address:
- element.initCursor(false);
+ element.leftSide = false;
+ element.initCursor();
assert.isNotOk(element.cursor.initialLineNumber);
// Does nothing when viewState specify side but no number:
- element.initCursor(true);
+ element.leftSide = true;
+ element.initCursor();
assert.isNotOk(element.cursor.initialLineNumber);
// Revision hash: specifies lineNum but not side.
element.focusLineNum = 234;
- element.initCursor(false);
+ element.leftSide = false;
+ element.initCursor();
assert.equal(element.cursor.initialLineNumber, 234);
assert.equal(element.cursor.side, Side.RIGHT);
// Base hash: specifies lineNum and side.
element.focusLineNum = 345;
- element.initCursor(true);
+ element.leftSide = true;
+ element.initCursor();
assert.equal(element.cursor.initialLineNumber, 345);
assert.equal(element.cursor.side, Side.LEFT);
// Specifies right side:
element.focusLineNum = 123;
- element.initCursor(false);
+ element.leftSide = false;
+ element.initCursor();
assert.equal(element.cursor.initialLineNumber, 123);
assert.equal(element.cursor.side, Side.RIGHT);
});
test('getLineOfInterest', () => {
- assert.isUndefined(element.getLineOfInterest(false));
+ element.leftSide = false;
+ assert.isUndefined(element.getLineOfInterest());
element.focusLineNum = 12;
- let result = element.getLineOfInterest(false);
+ element.leftSide = false;
+ let result = element.getLineOfInterest();
assert.isOk(result);
assert.equal(result!.lineNum, 12);
assert.equal(result!.side, Side.RIGHT);
- result = element.getLineOfInterest(true);
+ element.leftSide = true;
+ result = element.getLineOfInterest();
assert.isOk(result);
assert.equal(result!.lineNum, 12);
assert.equal(result!.side, Side.LEFT);
@@ -1907,10 +1433,8 @@
_number: 321 as NumericChangeId,
project: 'foo/bar' as RepoName,
};
- element.patchRange = {
- basePatchNum: 3 as BasePatchSetNum,
- patchNum: 5 as RevisionPatchSetNum,
- };
+ element.patchNum = 5 as RevisionPatchSetNum;
+ element.basePatchNum = 3 as BasePatchSetNum;
const e = {detail: {number: 123, side: Side.RIGHT}} as CustomEvent;
element.onLineSelected(e);
@@ -1931,10 +1455,8 @@
_number: 321 as NumericChangeId,
project: 'foo/bar' as RepoName,
};
- element.patchRange = {
- basePatchNum: 3 as BasePatchSetNum,
- patchNum: 5 as RevisionPatchSetNum,
- };
+ element.patchNum = 5 as RevisionPatchSetNum;
+ element.basePatchNum = 3 as BasePatchSetNum;
const e = {detail: {number: 123, side: Side.LEFT}} as CustomEvent;
element.onLineSelected(e);
@@ -1965,199 +1487,116 @@
});
});
- suite('initPatchRange', () => {
- setup(async () => {
- getDiffRestApiStub.returns(Promise.resolve(createDiff()));
- element.viewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- patchNum: 3 as RevisionPatchSetNum,
- path: 'abcd',
- };
- await element.updateComplete;
- });
- test('empty', () => {
- sinon.stub(element, 'getPaths').returns({});
- element.initPatchRange();
- assert.equal(Object.keys(element.commentMap ?? {}).length, 0);
- });
-
- test('has paths', () => {
- sinon.stub(element, 'fetchFiles');
- sinon.stub(element, 'getPaths').returns({
- 'path/to/file/one.cpp': true,
- 'path-to/file/two.py': true,
- });
- element.changeNum = 42 as NumericChangeId;
- element.patchRange = {
- basePatchNum: 3 as BasePatchSetNum,
- patchNum: 5 as RevisionPatchSetNum,
- };
- element.initPatchRange();
- assert.deepEqual(Object.keys(element.commentMap ?? {}), [
- 'path/to/file/one.cpp',
- 'path-to/file/two.py',
- ]);
- });
- });
-
- suite('computeCommentSkips', () => {
+ suite('findFileWithComment', () => {
test('empty file list', () => {
- const commentMap = {
- 'path/one.jpg': true,
- 'path/three.wav': true,
- };
- const path = 'path/two.m4v';
- const result = element.computeCommentSkips(commentMap, [], path);
- assert.isOk(result);
- assert.isNotOk(result!.previous);
- assert.isNotOk(result!.next);
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
+ element.path = 'path/two.m4v';
+ assert.isUndefined(element.findFileWithComment(-1));
+ assert.isUndefined(element.findFileWithComment(1));
});
test('finds skips', () => {
const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav'];
- let path = fileList[1];
- const commentMap: CommentMap = {};
- commentMap[fileList[0]] = true;
- commentMap[fileList[1]] = false;
- commentMap[fileList[2]] = true;
+ element.files = {sortedPaths: fileList, changeFilesByPath: {}};
+ element.path = fileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
- let result = element.computeCommentSkips(commentMap, fileList, path);
- assert.isOk(result);
- assert.equal(result!.previous, fileList[0]);
- assert.equal(result!.next, fileList[2]);
+ assert.equal(element.findFileWithComment(-1), fileList[0]);
+ assert.equal(element.findFileWithComment(1), fileList[2]);
- commentMap[fileList[1]] = true;
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/two.m4v': [createComment('c1', 1, 1, 'path/two.m4v')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
- result = element.computeCommentSkips(commentMap, fileList, path);
- assert.isOk(result);
- assert.equal(result!.previous, fileList[0]);
- assert.equal(result!.next, fileList[2]);
+ assert.equal(element.findFileWithComment(-1), fileList[0]);
+ assert.equal(element.findFileWithComment(1), fileList[2]);
- path = fileList[0];
+ element.path = fileList[0];
- result = element.computeCommentSkips(commentMap, fileList, path);
- assert.isOk(result);
- assert.isNull(result!.previous);
- assert.equal(result!.next, fileList[1]);
+ assert.isUndefined(element.findFileWithComment(-1));
+ assert.equal(element.findFileWithComment(1), fileList[1]);
- path = fileList[2];
+ element.path = fileList[2];
- result = element.computeCommentSkips(commentMap, fileList, path);
- assert.isOk(result);
- assert.equal(result!.previous, fileList[1]);
- assert.isNull(result!.next);
+ assert.equal(element.findFileWithComment(-1), fileList[1]);
+ assert.isUndefined(element.findFileWithComment(1));
});
suite('skip next/previous', () => {
- let navToChangeStub: SinonStub;
-
setup(() => {
- navToChangeStub = sinon.stub(element, 'navToChangeView');
element.files = getFilesFromFileList([
'path/one.jpg',
'path/two.m4v',
'path/three.wav',
]);
- element.patchRange = {
- patchNum: 2 as RevisionPatchSetNum,
- basePatchNum: 1 as BasePatchSetNum,
- };
+ element.patchNum = 2 as RevisionPatchSetNum;
+ element.basePatchNum = 1 as BasePatchSetNum;
});
- suite('moveToPreviousFileWithComment', () => {
- test('no skips', () => {
- element.moveToPreviousFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isFalse(setUrlStub.called);
- });
-
+ suite('moveToFileWithComment previous', () => {
test('no previous', async () => {
- const commentMap: CommentMap = {};
- commentMap[element.files.sortedFileList[0]!] = false;
- commentMap[element.files.sortedFileList[1]!] = false;
- commentMap[element.files.sortedFileList[2]!] = true;
- element.commentMap = commentMap;
- element.path = element.files.sortedFileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
+ element.path = element.files.sortedPaths[1];
await element.updateComplete;
- element.moveToPreviousFileWithComment();
+ element.moveToFileWithComment(-1);
assert.isTrue(navToChangeStub.calledOnce);
- assert.isFalse(setUrlStub.called);
+ assert.isFalse(navToDiffStub.called);
});
test('w/ previous', async () => {
- const commentMap: CommentMap = {};
- commentMap[element.files.sortedFileList[0]!] = true;
- commentMap[element.files.sortedFileList[1]!] = false;
- commentMap[element.files.sortedFileList[2]!] = true;
- element.commentMap = commentMap;
- element.path = element.files.sortedFileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
+ element.path = element.files.sortedPaths[1];
await element.updateComplete;
- element.moveToPreviousFileWithComment();
+ element.moveToFileWithComment(-1);
assert.isFalse(navToChangeStub.called);
- assert.isTrue(setUrlStub.calledOnce);
+ assert.isTrue(navToDiffStub.calledOnce);
});
});
- suite('moveToNextFileWithComment', () => {
- test('no skips', () => {
- element.moveToNextFileWithComment();
- assert.isFalse(navToChangeStub.called);
- assert.isFalse(setUrlStub.called);
- });
-
+ suite('moveToFileWithComment next', () => {
test('no previous', async () => {
- const commentMap: CommentMap = {};
- commentMap[element.files.sortedFileList[0]!] = true;
- commentMap[element.files.sortedFileList[1]!] = false;
- commentMap[element.files.sortedFileList[2]!] = false;
- element.commentMap = commentMap;
- element.path = element.files.sortedFileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ });
+ element.path = element.files.sortedPaths[1];
await element.updateComplete;
- element.moveToNextFileWithComment();
+ element.moveToFileWithComment(1);
assert.isTrue(navToChangeStub.calledOnce);
- assert.isFalse(setUrlStub.called);
+ assert.isFalse(navToDiffStub.called);
});
test('w/ previous', async () => {
- const commentMap: CommentMap = {};
- commentMap[element.files.sortedFileList[0]!] = true;
- commentMap[element.files.sortedFileList[1]!] = false;
- commentMap[element.files.sortedFileList[2]!] = true;
- element.commentMap = commentMap;
- element.path = element.files.sortedFileList[1];
+ element.changeComments = new ChangeComments({
+ 'path/one.jpg': [createComment('c1', 1, 1, 'path/one.jpg')],
+ 'path/three.wav': [createComment('c1', 1, 1, 'path/three.wav')],
+ });
+ element.path = element.files.sortedPaths[1];
await element.updateComplete;
- element.moveToNextFileWithComment();
+ element.moveToFileWithComment(1);
assert.isFalse(navToChangeStub.called);
- assert.isTrue(setUrlStub.calledOnce);
+ assert.isTrue(navToDiffStub.calledOnce);
});
});
});
});
- test('_computeEditMode', () => {
- const callCompute = (range: PatchRange) => {
- element.patchRange = range;
- return element.computeEditMode();
- };
- assert.isFalse(
- callCompute({
- basePatchNum: PARENT,
- patchNum: 1 as RevisionPatchSetNum,
- })
- );
- assert.isTrue(
- callCompute({
- basePatchNum: 1 as BasePatchSetNum,
- patchNum: EDIT,
- })
- );
- });
-
test('computeFileNum', () => {
element.path = '/foo';
assert.equal(
@@ -2224,15 +1663,18 @@
test('reviewed checkbox', async () => {
sinon.stub(element, 'handlePatchChange');
- element.patchRange = createPatchRange();
- await element.updateComplete;
- assertIsDefined(element.reviewed);
- // Reviewed checkbox should be shown.
- assert.isTrue(isVisible(element.reviewed));
- element.patchRange = {...element.patchRange, patchNum: EDIT};
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
await element.updateComplete;
- assert.isFalse(isVisible(element.reviewed));
+ let checkbox = queryAndAssert(element, '#reviewed');
+ assert.isTrue(isVisible(checkbox));
+
+ element.patchNum = EDIT;
+ await element.updateComplete;
+
+ checkbox = queryAndAssert(element, '#reviewed');
+ assert.isFalse(isVisible(checkbox));
});
});
@@ -2377,48 +1819,43 @@
test('File change should trigger setUrl once', async () => {
element.files = getFilesFromFileList(['file1', 'file2', 'file3']);
- sinon.stub(element, 'initLineOfInterestAndCursor');
+ sinon.stub(element, 'initCursor');
// Load file1
- element.viewState = {
- view: GerritView.DIFF,
+ viewModel.setState({
+ ...createDiffViewState(),
patchNum: 1 as RevisionPatchSetNum,
- changeNum: 101 as NumericChangeId,
repo: 'test-project' as RepoName,
- path: 'file1',
- };
- element.patchRange = {
- patchNum: 1 as RevisionPatchSetNum,
- basePatchNum: PARENT,
- };
+ diffView: {path: 'file1'},
+ });
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.change = {
...createParsedChange(),
revisions: createRevisions(1),
};
await element.updateComplete;
- assert.isFalse(setUrlStub.called);
+ assert.isFalse(navToDiffStub.called);
// Switch to file2
element.handleFileChange(
new CustomEvent('value-change', {detail: {value: 'file2'}})
);
- assert.isTrue(setUrlStub.calledOnce);
+ assert.isTrue(navToDiffStub.calledOnce);
+ assert.deepEqual(navToDiffStub.lastCall.firstArg, {path: 'file2'});
// This is to mock the param change triggered by above navigate
- element.viewState = {
- view: GerritView.DIFF,
+ viewModel.setState({
+ ...createDiffViewState(),
patchNum: 1 as RevisionPatchSetNum,
- changeNum: 101 as NumericChangeId,
repo: 'test-project' as RepoName,
- path: 'file2',
- };
- element.patchRange = {
- patchNum: 1 as RevisionPatchSetNum,
- basePatchNum: PARENT,
- };
+ diffView: {path: 'file2'},
+ });
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
// No extra call
- assert.isTrue(setUrlStub.calledOnce);
+ assert.isTrue(navToDiffStub.calledOnce);
});
test('_computeDownloadDropdownLinks', () => {
@@ -2440,10 +1877,8 @@
element.change = createParsedChange();
element.change.project = 'test' as RepoName;
element.changeNum = 12 as NumericChangeId;
- element.patchRange = {
- patchNum: 1 as RevisionPatchSetNum,
- basePatchNum: PARENT,
- };
+ element.patchNum = 1 as RevisionPatchSetNum;
+ element.basePatchNum = PARENT;
element.path = 'index.php';
element.diff = createDiff();
assert.deepEqual(element.computeDownloadDropdownLinks(), downloadLinks);
@@ -2472,10 +1907,8 @@
element.change = createParsedChange();
element.change.project = 'test' as RepoName;
element.changeNum = 12 as NumericChangeId;
- element.patchRange = {
- patchNum: 3 as RevisionPatchSetNum,
- basePatchNum: 2 as BasePatchSetNum,
- };
+ element.patchNum = 3 as RevisionPatchSetNum;
+ element.basePatchNum = 2 as BasePatchSetNum;
element.path = 'index.php';
element.diff = diff;
assert.deepEqual(element.computeDownloadDropdownLinks(), downloadLinks);
@@ -2539,49 +1972,4 @@
);
});
});
-
- suite('unmodified files with comments', () => {
- let element: GrDiffView;
-
- setup(async () => {
- const changedFiles = {
- 'file1.txt': createFileInfo(),
- 'a/b/test.c': createFileInfo(),
- };
- stubRestApi('getConfig').returns(Promise.resolve(createServerInfo()));
- stubRestApi('getProjectConfig').returns(Promise.resolve(createConfig()));
- stubRestApi('getChangeFiles').returns(Promise.resolve(changedFiles));
- stubRestApi('saveFileReviewed').returns(Promise.resolve(new Response()));
- stubRestApi('getDiffComments').returns(Promise.resolve({}));
- stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
- stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
- stubRestApi('getReviewedFiles').returns(Promise.resolve([]));
- element = await fixture(html`<gr-diff-view></gr-diff-view>`);
- element.changeNum = 42 as NumericChangeId;
- });
-
- test('fetchFiles add files with comments without changes', () => {
- element.patchRange = {
- basePatchNum: 5 as BasePatchSetNum,
- patchNum: 10 as RevisionPatchSetNum,
- };
- element.changeComments = {
- getPaths: sinon.stub().returns({
- 'file2.txt': {},
- 'file1.txt': {},
- }),
- } as unknown as ChangeComments;
- element.changeNum = 23 as NumericChangeId;
- return element.fetchFiles().then(() => {
- assert.deepEqual(element.files, {
- sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'],
- changeFilesByPath: {
- 'file1.txt': createFileInfo(),
- 'file2.txt': {status: 'U'} as FileInfo,
- 'a/b/test.c': createFileInfo(),
- },
- });
- });
- });
- });
});
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index d9f88f7..325798c 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -21,6 +21,7 @@
import {
BasePatchSetNum,
EDIT,
+ NumericChangeId,
PARENT,
PatchSetNum,
RevisionInfo,
@@ -36,7 +37,7 @@
import {EditRevisionInfo} from '../../../types/types';
import {a11yStyles} from '../../../styles/gr-a11y-styles';
import {sharedStyles} from '../../../styles/shared-styles';
-import {LitElement, PropertyValues, css, html} from 'lit';
+import {LitElement, css, html, nothing} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {subscribe} from '../../lit/subscription-controller';
import {commentsModelToken} from '../../../models/comments/comments-model';
@@ -44,6 +45,8 @@
import {ifDefined} from 'lit/directives/if-defined.js';
import {ValueChangedEvent} from '../../../types/events';
import {GeneratedWebLink} from '../../../utils/weblink-util';
+import {changeModelToken} from '../../../models/change/change-model';
+import {changeViewModelToken} from '../../../models/views/change';
// Maximum length for patch set descriptions.
const PATCH_DESC_MAX_LENGTH = 500;
@@ -83,33 +86,27 @@
@query('#patchNumDropdown')
patchNumDropdown?: GrDropdownList;
- @property({type: Array})
- availablePatches?: PatchSet[];
+ @state()
+ availablePatches: PatchSet[] = [];
- @property({type: String})
- changeNum?: string;
+ @state()
+ changeNum?: NumericChangeId;
@property({type: Object})
filesWeblinks?: FilesWebLinks;
- @property({type: String})
+ @state()
patchNum?: RevisionPatchSetNum;
- @property({type: String})
+ @state()
basePatchNum?: BasePatchSetNum;
- /** Not used directly. Translated into `sortedRevisions` in willUpdate(). */
- @property({type: Object})
- revisions: (RevisionInfo | EditRevisionInfo)[] = [];
-
- @property({type: Object})
+ @state()
revisionInfo?: RevisionInfoClass;
- /** Private internal state, derived from `revisions` in willUpdate(). */
@state()
- private sortedRevisions: (RevisionInfo | EditRevisionInfo)[] = [];
+ sortedRevisions: (RevisionInfo | EditRevisionInfo)[] = [];
- /** Private internal state, visible for testing. */
@state()
changeComments?: ChangeComments;
@@ -118,10 +115,44 @@
private readonly getCommentsModel = resolve(this, commentsModelToken);
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
+ private readonly getViewModel = resolve(this, changeViewModelToken);
+
constructor() {
super();
subscribe(
this,
+ () => this.getViewModel().changeNum$,
+ x => (this.changeNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().change$,
+ x => (this.revisionInfo = x ? new RevisionInfoClass(x) : undefined)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().patchNum$,
+ x => (this.patchNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().basePatchNum$,
+ x => (this.basePatchNum = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().patchsets$,
+ x => (this.availablePatches = x)
+ );
+ subscribe(
+ this,
+ () => this.getChangeModel().revisions$,
+ x => (this.sortedRevisions = sortRevisions(Object.values(x || {})))
+ );
+ subscribe(
+ this,
() => this.getCommentsModel().changeComments$,
x => (this.changeComments = x)
);
@@ -164,6 +195,9 @@
}
override render() {
+ if (!this.changeNum || !this.patchNum || !this.basePatchNum) {
+ return nothing;
+ }
return html`
<h3 class="assistive-tech-only">Patchset Range Selection</h3>
<span class="patchRange" aria-label="patch range starts with">
@@ -203,16 +237,9 @@
> `;
}
- override willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has('revisions')) {
- this.sortedRevisions = sortRevisions(Object.values(this.revisions || {}));
- }
- }
-
// Private method, but visible for testing.
computeBaseDropdownContent(): DropdownItem[] {
if (
- this.availablePatches === undefined ||
this.patchNum === undefined ||
this.changeComments === undefined ||
this.revisionInfo === undefined
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
index c90992f..584fcd7 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.ts
@@ -9,7 +9,7 @@
import {GrPatchRangeSelect} from './gr-patch-range-select';
import {RevisionInfo as RevisionInfoClass} from '../../shared/revision-info/revision-info';
import {ChangeComments} from '../gr-comment-api/gr-comment-api';
-import {stubReporting, stubRestApi} from '../../../test/test-utils';
+import {stubReporting} from '../../../test/test-utils';
import {
BasePatchSetNum,
EDIT,
@@ -25,8 +25,11 @@
import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
import {SpecialFilePath} from '../../../constants/constants';
import {
+ createChangeViewState,
createEditRevision,
+ createParsedChange,
createRevision,
+ createRevisions,
} from '../../../test/test-data-generators';
import {PatchSet} from '../../../utils/patch-set-util';
import {
@@ -36,6 +39,9 @@
import {queryAndAssert} from '../../../test/test-utils';
import {fire} from '../../../utils/event-util';
import {fixture, html, assert} from '@open-wc/testing';
+import {testResolver} from '../../../test/common-test-setup';
+import {changeViewModelToken} from '../../../models/views/change';
+import {changeModelToken} from '../../../models/change/change-model';
type RevIdToRevisionInfo = {
[revisionId: string]: RevisionInfo | EditRevisionInfo;
@@ -53,16 +59,23 @@
}
setup(async () => {
- stubRestApi('getDiffComments').returns(Promise.resolve({}));
- stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
- stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
-
// Element must be wrapped in an element with direct access to the
// comment API.
element = await fixture(
html`<gr-patch-range-select></gr-patch-range-select>`
);
+ const viewModel = testResolver(changeViewModelToken);
+ viewModel.setState({
+ ...createChangeViewState(),
+ patchNum: 1 as RevisionPatchSetNum,
+ basePatchNum: PARENT,
+ });
+ const changeModel = testResolver(changeModelToken);
+ changeModel.updateStateChange({
+ ...createParsedChange(),
+ revisions: createRevisions(5),
+ });
// Stub methods on the changeComments object after changeComments has
// been initialized.
element.changeComments = new ChangeComments();
@@ -86,7 +99,7 @@
});
test('enabled/disabled options', async () => {
- element.revisions = [
+ element.sortedRevisions = [
createRevision(3),
createEditRevision(2),
createRevision(2),
@@ -119,13 +132,13 @@
{num: 2, sha: '3'} as PatchSet,
{num: 1, sha: '4'} as PatchSet,
];
- element.revisions = [
- createRevision(2),
- createRevision(3),
- createRevision(1),
+ element.sortedRevisions = [
createRevision(4),
+ createRevision(3),
+ createRevision(2),
+ createRevision(1),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
const expectedResult: DropdownItem[] = [
{
disabled: true,
@@ -175,13 +188,13 @@
});
test('computeBaseDropdownContent called when patchNum updates', async () => {
- element.revisions = [
- createRevision(2),
- createRevision(3),
- createRevision(1),
+ element.sortedRevisions = [
createRevision(4),
+ createRevision(3),
+ createRevision(2),
+ createRevision(1),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
element.availablePatches = [
{num: 1, sha: '1'} as PatchSet,
{num: 2, sha: '2'} as PatchSet,
@@ -201,13 +214,13 @@
});
test('computeBaseDropdownContent called when changeComments update', async () => {
- element.revisions = [
- createRevision(2),
- createRevision(3),
- createRevision(1),
+ element.sortedRevisions = [
createRevision(4),
+ createRevision(3),
+ createRevision(2),
+ createRevision(1),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
element.availablePatches = [
{num: 3, sha: '2'} as PatchSet,
{num: 2, sha: '3'} as PatchSet,
@@ -226,13 +239,13 @@
});
test('computePatchDropdownContent called when basePatchNum updates', async () => {
- element.revisions = [
+ element.sortedRevisions = [
createRevision(2),
createRevision(3),
createRevision(1),
createRevision(4),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
element.availablePatches = [
{num: 1, sha: '1'} as PatchSet,
{num: 2, sha: '2'} as PatchSet,
@@ -258,7 +271,7 @@
{num: 1, sha: '4'} as PatchSet,
];
element.basePatchNum = 1 as BasePatchSetNum;
- element.revisions = [
+ element.sortedRevisions = [
createRevision(3),
createEditRevision(2),
createRevision(2, 'description'),
@@ -402,13 +415,13 @@
{num: 2, sha: '3'} as PatchSet,
{num: 1, sha: '4'} as PatchSet,
];
- element.revisions = [
+ element.sortedRevisions = [
createRevision(2),
createRevision(3),
createRevision(1),
createRevision(4),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
await element.updateComplete;
element.addEventListener('patch-range-change', handler);
@@ -444,13 +457,13 @@
{num: 2, sha: '3'} as PatchSet,
{num: 1, sha: '4'} as PatchSet,
];
- element.revisions = [
+ element.sortedRevisions = [
createRevision(2),
createRevision(3),
createRevision(1),
createRevision(4),
];
- element.revisionInfo = getInfo(element.revisions);
+ element.revisionInfo = getInfo(element.sortedRevisions);
element.patchNum = 1 as PatchSetNumber;
element.basePatchNum = PARENT;
await element.updateComplete;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
index d1d05b7..ec1e48e 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.ts
@@ -29,7 +29,7 @@
import {customElement, property, query, state} from 'lit/decorators.js';
import {BindValueChangeEvent} from '../../../types/events';
import {IronInputElement} from '@polymer/iron-input/iron-input';
-import {createEditUrl} from '../../../models/views/edit';
+import {createEditUrl} from '../../../models/views/change';
import {resolve} from '../../../models/dependency';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {whenVisible} from '../../../utils/dom-util';
@@ -429,8 +429,8 @@
const url = createEditUrl({
changeNum: this.change._number,
repo: this.change.project,
- path: this.path,
patchNum: this.patchNum,
+ editView: {path: this.path},
});
this.getNavigation().setUrl(url);
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 8e346c3..acc4c9e 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -31,8 +31,12 @@
import {resolve} from '../../../models/dependency';
import {changeModelToken} from '../../../models/change/change-model';
import {ShortcutController} from '../../lit/shortcut-controller';
-import {editViewModelToken, EditViewState} from '../../../models/views/edit';
-import {createChangeUrl} from '../../../models/views/change';
+import {
+ ChangeChildView,
+ changeViewModelToken,
+ ChangeViewState,
+ createChangeUrl,
+} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
@@ -60,7 +64,7 @@
*/
@property({type: Object})
- viewState?: EditViewState;
+ viewState?: ChangeViewState;
// private but used in test
@state() change?: ParsedChangeInfo;
@@ -95,7 +99,7 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly getEditViewModel = resolve(this, editViewModelToken);
+ private readonly getViewModel = resolve(this, changeViewModelToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -116,8 +120,10 @@
);
subscribe(
this,
- () => this.getEditViewModel().state$,
+ () => this.getViewModel().state$,
state => {
+ // TODO: Add a setter for `viewState` instead of relying on the
+ // `viewStateChanged()` call here.
this.viewState = state;
this.viewStateChanged();
}
@@ -206,7 +212,7 @@
}
override render() {
- if (!this.viewState) return;
+ if (this.viewState?.childView !== ChangeChildView.EDIT) return nothing;
return html` ${this.renderHeader()} ${this.renderEndpoint()} `;
}
@@ -220,7 +226,7 @@
<span class="separator"></span>
<gr-editable-label
labelText="File path"
- .value=${this.viewState?.path}
+ .value=${this.viewState?.editView?.path}
placeholder="File path..."
@changed=${this.handlePathChanged}
></gr-editable-label>
@@ -277,7 +283,7 @@
></gr-endpoint-param>
<gr-endpoint-param
name="lineNum"
- .value=${this.viewState?.lineNum}
+ .value=${this.viewState?.editView?.lineNum}
></gr-endpoint-param>
<gr-default-editor
id="file"
@@ -298,19 +304,21 @@
}
get storageKey() {
- return `c${this.viewState?.changeNum}_ps${this.viewState?.patchNum}_${this.viewState?.path}`;
+ return `c${this.viewState?.changeNum}_ps${this.viewState?.patchNum}_${this.viewState?.editView?.path}`;
}
// private but used in test
viewStateChanged() {
- if (!this.viewState) return;
+ if (this.viewState?.childView !== ChangeChildView.EDIT) return;
// NOTE: This may be called before attachment (e.g. while parentElement is
// null). Fire title-change in an async so that, if attachment to the DOM
// has been queued, the event can bubble up to the handler in gr-app.
setTimeout(() => {
if (!this.viewState) return;
- const title = `Editing ${computeTruncatedPath(this.viewState.path)}`;
+ const title = `Editing ${computeTruncatedPath(
+ this.viewState.editView?.path
+ )}`;
fireTitleChange(this, title);
});
@@ -347,7 +355,7 @@
// private but used in test
async handlePathChanged(e: CustomEvent<string>): Promise<void> {
const changeNum = this.viewState?.changeNum;
- const currentPath = this.viewState?.path;
+ const currentPath = this.viewState?.editView?.path;
assertIsDefined(changeNum, 'change number');
assertIsDefined(currentPath, 'path');
@@ -376,7 +384,7 @@
getFileData() {
const changeNum = this.viewState?.changeNum;
const patchNum = this.viewState?.patchNum;
- const path = this.viewState?.path;
+ const path = this.viewState?.editView?.path;
assertIsDefined(changeNum, 'change number');
assertIsDefined(patchNum, 'patchset number');
assertIsDefined(path, 'path');
@@ -416,7 +424,7 @@
// private but used in test
saveEdit() {
const changeNum = this.viewState?.changeNum;
- const path = this.viewState?.path;
+ const path = this.viewState?.editView?.path;
assertIsDefined(changeNum, 'change number');
assertIsDefined(path, 'path');
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index d428e18..1a4879d 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -72,7 +72,7 @@
labeltext="File path"
placeholder="File path..."
tabindex="0"
- title="${element.viewState?.path}"
+ title="${element.viewState?.editView?.path}"
>
</gr-editable-label>
</span>
@@ -373,7 +373,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: EDIT,
- path: 'test/path',
+ editView: {path: 'test/path'},
};
// Ensure no data is set with a bad response.
@@ -392,7 +392,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: EDIT,
- path: 'test/path',
+ editView: {path: 'test/path'},
};
// Ensure no data is set with a bad response.
@@ -415,7 +415,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: EDIT,
- path: 'test/path',
+ editView: {path: 'test/path'},
};
return element.getFileData().then(() => {
@@ -433,7 +433,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: EDIT,
- path: 'test/path',
+ editView: {path: 'test/path'},
};
return element.getFileData().then(() => {
@@ -530,7 +530,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: 1 as RevisionPatchSetNum,
- path: 'test',
+ editView: {path: 'test'},
};
const alertStub = sinon.stub();
@@ -562,7 +562,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: 1 as RevisionPatchSetNum,
- path: 'test',
+ editView: {path: 'test'},
};
const alertStub = sinon.stub();
@@ -583,7 +583,7 @@
...createEditViewState(),
changeNum: 1 as NumericChangeId,
patchNum: 1 as RevisionPatchSetNum,
- path: 'test',
+ editView: {path: 'test'},
};
assert.equal(element.storageKey, 'c1_ps1_test');
});
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index c00def6..eceb05e 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -23,7 +23,6 @@
import './plugins/gr-endpoint-decorator/gr-endpoint-decorator';
import './plugins/gr-endpoint-param/gr-endpoint-param';
import './plugins/gr-endpoint-slot/gr-endpoint-slot';
-import './plugins/gr-external-style/gr-external-style';
import './plugins/gr-plugin-host/gr-plugin-host';
import './settings/gr-cla-view/gr-cla-view';
import './settings/gr-registration-dialog/gr-registration-dialog';
@@ -74,6 +73,8 @@
import {createDashboardUrl} from '../models/views/dashboard';
import {userModelToken} from '../models/user/user-model';
import {modalStyles} from '../styles/gr-modal-styles';
+import {AdminChildView, createAdminUrl} from '../models/views/admin';
+import {ChangeChildView, changeViewModelToken} from '../models/views/change';
interface ErrorInfo {
text: string;
@@ -119,6 +120,9 @@
@state() private view?: GerritView;
+ // TODO: Introduce a wrapper element for CHANGE, DIFF, EDIT view.
+ @state() private childView?: ChangeChildView;
+
@state() private lastError?: ErrorInfo;
// private but used in test
@@ -152,8 +156,6 @@
@state() private theme = AppTheme.AUTO;
- @state() private themeEndpoint = 'app-theme-light';
-
readonly getRouter = resolve(this, routerToken);
private readonly getNavigation = resolve(this, navigationToken);
@@ -170,6 +172,8 @@
private readonly getRouterModel = resolve(this, routerModelToken);
+ private readonly getChangeViewModel = resolve(this, changeViewModelToken);
+
constructor() {
super();
@@ -212,6 +216,16 @@
createSearchUrl({query: 'is:watched is:open'})
)
);
+ this.shortcuts.addAbstract(Shortcut.GO_TO_REPOS, () =>
+ this.getNavigation().setUrl(
+ createAdminUrl({adminView: AdminChildView.REPOS})
+ )
+ );
+ this.shortcuts.addAbstract(Shortcut.GO_TO_GROUPS, () =>
+ this.getNavigation().setUrl(
+ createAdminUrl({adminView: AdminChildView.GROUPS})
+ )
+ );
subscribe(
this,
@@ -229,6 +243,13 @@
if (view) this.errorView?.classList.remove('show');
}
);
+ subscribe(
+ this,
+ () => this.getChangeViewModel().childView$,
+ childView => {
+ this.childView = childView;
+ }
+ );
prefersDarkColorScheme().addEventListener('change', () => {
if (this.theme === AppTheme.AUTO) {
@@ -374,14 +395,6 @@
.loginUrl=${this.loginUrl}
></gr-error-manager>
<gr-plugin-host id="plugins"></gr-plugin-host>
- <gr-external-style
- id="externalStyleForAll"
- name="app-theme"
- ></gr-external-style>
- <gr-external-style
- id="externalStyleForTheme"
- name=${this.themeEndpoint}
- ></gr-external-style>
`;
}
@@ -464,9 +477,7 @@
this.updateComplete.then(() => (this.invalidateChangeViewCache = false));
return nothing;
}
- return cache(
- this.view === GerritView.CHANGE ? this.changeViewTemplate() : nothing
- );
+ return cache(this.isChangeView() ? this.changeViewTemplate() : nothing);
}
// Template as not to create duplicates, for renderChangeView() only.
@@ -476,8 +487,27 @@
`;
}
+ private isChangeView() {
+ return (
+ this.view === GerritView.CHANGE &&
+ this.childView === ChangeChildView.OVERVIEW
+ );
+ }
+
+ private isDiffView() {
+ return (
+ this.view === GerritView.CHANGE && this.childView === ChangeChildView.DIFF
+ );
+ }
+
+ private isEditorView() {
+ return (
+ this.view === GerritView.CHANGE && this.childView === ChangeChildView.EDIT
+ );
+ }
+
private renderEditorView() {
- if (this.view !== GerritView.EDIT) return nothing;
+ if (!this.isEditorView()) return nothing;
return html`<gr-editor-view></gr-editor-view>`;
}
@@ -486,9 +516,7 @@
this.updateComplete.then(() => (this.invalidateDiffViewCache = false));
return nothing;
}
- return cache(
- this.view === GerritView.DIFF ? this.diffViewTemplate() : nothing
- );
+ return cache(this.isDiffView() ? this.diffViewTemplate() : nothing);
}
private diffViewTemplate() {
@@ -622,11 +650,12 @@
const showDarkTheme = isDarkTheme(this.theme);
document.documentElement.classList.toggle('darkTheme', showDarkTheme);
document.documentElement.classList.toggle('lightTheme', !showDarkTheme);
+ // TODO: Remove this code for adding/removing dark theme style. We should
+ // be able to just always add them once we have changed its css selector
+ // from `html` to `html.darkTheme`.
if (showDarkTheme) {
- this.themeEndpoint = 'app-theme-dark';
applyDarkTheme();
} else {
- this.themeEndpoint = 'app-theme-light';
removeDarkTheme();
}
}
diff --git a/polygerrit-ui/app/elements/gr-app-types.ts b/polygerrit-ui/app/elements/gr-app-types.ts
index 3008236..0261992 100644
--- a/polygerrit-ui/app/elements/gr-app-types.ts
+++ b/polygerrit-ui/app/elements/gr-app-types.ts
@@ -13,8 +13,6 @@
import {SearchViewState} from '../models/views/search';
import {DashboardViewState} from '../models/views/dashboard';
import {ChangeViewState} from '../models/views/change';
-import {DiffViewState} from '../models/views/diff';
-import {EditViewState} from '../models/views/edit';
export interface AppElement extends HTMLElement {
params: AppElementParams;
@@ -41,8 +39,6 @@
| SearchViewState
| SettingsViewState
| AgreementViewState
- | DiffViewState
- | EditViewState
| AppElementJustRegisteredParams;
export function isAppElementJustRegisteredParams(
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.ts b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.ts
index d5fc9f8..9cacaea 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.ts
@@ -5,7 +5,10 @@
*/
import {html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
-import {ModuleInfo} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
+import {
+ EndpointType,
+ ModuleInfo,
+} from '../../shared/gr-js-api-interface/gr-plugin-endpoints';
import {PluginApi} from '../../../api/plugin';
import {HookApi, PluginElement} from '../../../api/hook';
import {getAppContext} from '../../../services/app-context';
@@ -195,10 +198,10 @@
}
let initPromise;
switch (type) {
- case 'decorate':
+ case EndpointType.DECORATE:
initPromise = this.initDecoration(moduleName, plugin, slot);
break;
- case 'replace':
+ case EndpointType.REPLACE:
initPromise = this.initReplacement(moduleName, plugin);
break;
}
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.ts b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.ts
deleted file mode 100644
index 5a0e8df..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {updateStyles} from '@polymer/polymer/lib/mixins/element-mixin';
-import {LitElement, html, PropertyValues} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
-import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-import {resolve} from '../../../models/dependency';
-
-@customElement('gr-external-style')
-export class GrExternalStyle extends LitElement {
- // This is a required value for this component.
- @property({type: String, reflect: true})
- name!: string;
-
- // private but used in test
- stylesApplied: string[] = [];
-
- stylesElements: HTMLElement[] = [];
-
- private readonly getPluginLoader = resolve(this, pluginLoaderToken);
-
- override render() {
- return html`<slot></slot>`;
- }
-
- override willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has('name')) {
- // We remove all styles defined for different name.
- this.removeStyles();
- this.importAndApply();
- this.getPluginLoader()
- .awaitPluginsLoaded()
- .then(() => this.importAndApply());
- }
- }
-
- // private but used in test
- applyStyle(name: string) {
- if (this.stylesApplied.includes(name)) {
- return;
- }
- this.stylesApplied.push(name);
-
- const s = document.createElement('style');
- s.setAttribute('include', name);
- const cs = document.createElement('custom-style');
- this.stylesElements.push(cs);
- cs.appendChild(s);
- // When using Shadow DOM <custom-style> must be added to the <body>.
- // Within <gr-external-style> itself the styles would have no effect.
- const topEl = document.getElementsByTagName('body')[0];
- topEl.insertBefore(cs, topEl.firstChild);
- updateStyles();
- }
-
- removeStyles() {
- this.stylesElements.forEach(el => el.remove());
- this.stylesElements = [];
- this.stylesApplied = [];
- }
-
- private importAndApply() {
- const moduleNames = this.getPluginLoader().pluginEndPoints.getModules(
- this.name
- );
- for (const name of moduleNames) {
- this.applyStyle(name);
- }
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-external-style': GrExternalStyle;
- }
-}
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.ts b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.ts
deleted file mode 100644
index 6c0e789..0000000
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import {mockPromise, MockPromise} from '../../../test/test-utils';
-import './gr-external-style';
-import {GrExternalStyle} from './gr-external-style';
-import {PluginApi} from '../../../api/plugin';
-import {fixture, html, assert} from '@open-wc/testing';
-import {testResolver} from '../../../test/common-test-setup';
-import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
-
-suite('gr-external-style integration tests', () => {
- const TEST_URL = 'http://some.com/plugins/url.js';
-
- let element: GrExternalStyle;
- let plugin: PluginApi | undefined;
- let pluginsLoaded: MockPromise<void>;
- let applyStyleSpy: sinon.SinonSpy;
-
- const installPlugin = () => {
- if (plugin) {
- return;
- }
- window.Gerrit.install(
- p => {
- plugin = p;
- },
- '0.1',
- TEST_URL
- );
- };
-
- const createElement = async () => {
- applyStyleSpy = sinon.spy(GrExternalStyle.prototype, 'applyStyle');
- element = await fixture(
- html`<gr-external-style .name=${'foo'}></gr-external-style>`
- );
- await element.updateComplete;
- };
-
- /**
- * Installs the plugin, creates the element, registers style module.
- */
- const lateRegister = async () => {
- installPlugin();
- await createElement();
- plugin!.registerStyleModule('foo', 'some-module');
- };
-
- /**
- * Installs the plugin, registers style module, creates the element.
- */
- const earlyRegister = async () => {
- installPlugin();
- plugin!.registerStyleModule('foo', 'some-module');
- await createElement();
- };
-
- setup(() => {
- pluginsLoaded = mockPromise();
- sinon
- .stub(testResolver(pluginLoaderToken), 'awaitPluginsLoaded')
- .returns(pluginsLoaded);
- });
-
- teardown(() => {
- plugin = undefined;
- document.body
- .querySelectorAll('custom-style')
- .forEach(style => style.remove());
- });
-
- test('applies plugin-provided styles', async () => {
- await lateRegister();
- pluginsLoaded.resolve();
- await element.updateComplete;
- assert.isTrue(applyStyleSpy.calledWith('some-module'));
- });
-
- test('does not double apply', async () => {
- await earlyRegister();
- await element.updateComplete;
- plugin!.registerStyleModule('foo', 'some-module');
- await element.updateComplete;
- const stylesApplied = element.stylesApplied.filter(
- name => name === 'some-module'
- );
- assert.strictEqual(stylesApplied.length, 1);
- });
-
- test('loads and applies preloaded modules', async () => {
- await earlyRegister();
- await element.updateComplete;
- assert.isTrue(applyStyleSpy.calledWith('some-module'));
- });
-
- test('removes old custom-style if name is changed', async () => {
- installPlugin();
- plugin!.registerStyleModule('bar', 'some-module');
- await earlyRegister();
- await element.updateComplete;
- let customStyles = document.body.querySelectorAll('custom-style');
- assert.strictEqual(customStyles.length, 1);
- element.name = 'bar';
- await element.updateComplete;
- customStyles = document.body.querySelectorAll('custom-style');
- assert.strictEqual(customStyles.length, 1);
- element.name = 'baz';
- await element.updateComplete;
- customStyles = document.body.querySelectorAll('custom-style');
- assert.strictEqual(customStyles.length, 0);
- });
-
- test('can apply more than one style', async () => {
- await earlyRegister();
- await element.updateComplete;
- plugin!.registerStyleModule('foo', 'some-module2');
- pluginsLoaded.resolve();
- await element.updateComplete;
- assert.strictEqual(element.stylesApplied.length, 2);
- const customStyles = document.body.querySelectorAll('custom-style');
- assert.strictEqual(customStyles.length, 2);
- });
-});
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
index 1549da5..0e75abb 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.ts
@@ -95,12 +95,26 @@
.lengthCounter {
font-weight: var(--font-weight-normal);
}
+ p {
+ max-width: 65ch;
+ margin-bottom: var(--spacing-m);
+ }
`,
];
override render() {
if (!this.account || this.loading) return nothing;
return html`<div class="gr-form-styles">
+ <p>
+ All profile fields below may be publicly displayed to others, including
+ on changes you are associated with, as well as in search and
+ autocompletion.
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-privacy.html"
+ >Learn more</a
+ >
+ </p>
+ <gr-endpoint-decorator name="profile"></gr-endpoint-decorator>
<section>
<span class="title"></span>
<span class="value">
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
index e968b12..f954960 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.ts
@@ -69,6 +69,16 @@
element,
/* HTML */ `
<div class="gr-form-styles">
+ <p>
+ All profile fields below may be publicly displayed to others,
+ including on changes you are associated with, as well as in search
+ and autocompletion.
+ <a
+ href="https://gerrit-review.googlesource.com/Documentation/user-privacy.html"
+ >Learn more</a
+ >
+ </p>
+ <gr-endpoint-decorator name="profile"></gr-endpoint-decorator>
<section>
<span class="title"></span>
<span class="value">
@@ -134,7 +144,8 @@
</span>
</section>
</div>
- `
+ `,
+ {ignoreChildren: ['p']}
);
});
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
index 17ac2a6..9c323aa 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.ts
@@ -6,7 +6,6 @@
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import '../../shared/gr-overlay/gr-overlay';
import {SshKeyInfo} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index df41b1f4..dd0fbca 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -72,8 +72,7 @@
import {whenRendered} from '../../../utils/dom-util';
import {Interaction} from '../../../constants/reporting';
import {HtmlPatched} from '../../../utils/lit-util';
-import {createDiffUrl} from '../../../models/views/diff';
-import {createChangeUrl} from '../../../models/views/change';
+import {createChangeUrl, createDiffUrl} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {highlightServiceToken} from '../../../services/highlight/highlight-service';
@@ -746,8 +745,8 @@
return createDiffUrl({
changeNum: this.changeNum,
repo: this.repoName,
- path: this.thread.path,
patchNum: this.thread.patchNum,
+ diffView: {path: this.thread.path},
});
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 5306cea..c1cc863 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -60,7 +60,7 @@
import {Interaction} from '../../../constants/reporting';
import {KnownExperimentId} from '../../../services/flags/flags';
import {isBase64FileContent} from '../../../api/rest-api';
-import {createDiffUrl} from '../../../models/views/diff';
+import {createDiffUrl} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {modalStyles} from '../../../styles/gr-modal-styles';
@@ -130,6 +130,9 @@
@query('#confirmDeleteModal')
confirmDeleteModal?: HTMLDialogElement;
+ @query('#confirmDeleteCommentDialog')
+ confirmDeleteDialog?: GrConfirmDeleteCommentDialog;
+
@property({type: Object})
comment?: Comment;
@@ -200,9 +203,6 @@
unresolved = true;
@property({type: Boolean})
- showConfirmDeleteModal = false;
-
- @property({type: Boolean})
unableToSave = false;
@property({type: Boolean, attribute: 'show-patchset'})
@@ -275,9 +275,7 @@
this.save();
});
}
- if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
- this.messagePlaceholder = 'Mention others with @';
- }
+ this.messagePlaceholder = 'Mention others with @';
subscribe(
this,
() => this.getUserModel().account$,
@@ -547,8 +545,9 @@
${this.renderDraftLabel()}
</div>
<div class="headerMiddle">${this.renderCollapsedContent()}</div>
- ${this.renderRunDetails()} ${this.renderDeleteButton()}
- ${this.renderPatchset()} ${this.renderDate()} ${this.renderToggle()}
+ ${this.renderSuggestEditButton()} ${this.renderRunDetails()}
+ ${this.renderDeleteButton()} ${this.renderPatchset()}
+ ${this.renderDate()} ${this.renderToggle()}
</div>
`;
}
@@ -644,7 +643,10 @@
title="Delete Comment"
link
class="action delete"
- @click=${this.openDeleteCommentModal}
+ @click=${(e: MouseEvent) => {
+ e.stopPropagation();
+ this.openDeleteCommentModal();
+ }}
>
<gr-icon id="icon" icon="delete" filled></gr-icon>
</gr-button>
@@ -774,10 +776,9 @@
return html`
<div class="rightActions">
${this.autoSaving ? html`. ` : ''}
- ${this.renderDiscardButton()} ${this.renderSuggestEditButton()}
- ${this.renderPreviewSuggestEditButton()} ${this.renderEditButton()}
- ${this.renderCancelButton()} ${this.renderSaveButton()}
- ${this.renderCopyLinkIcon()}
+ ${this.renderDiscardButton()} ${this.renderPreviewSuggestEditButton()}
+ ${this.renderEditButton()} ${this.renderCancelButton()}
+ ${this.renderSaveButton()} ${this.renderCopyLinkIcon()}
</div>
`;
}
@@ -806,6 +807,7 @@
return nothing;
}
if (
+ !this.editing ||
this.permanentEditingMode ||
this.comment?.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS
) {
@@ -937,11 +939,10 @@
}
private renderConfirmDialog() {
- if (!this.showConfirmDeleteModal) return;
return html`
<dialog id="confirmDeleteModal" tabindex="-1">
<gr-confirm-delete-comment-dialog
- id="confirmDeleteComment"
+ id="confirmDeleteCommentDialog"
@confirm=${this.handleConfirmDeleteComment}
@cancel=${this.closeDeleteCommentModal}
>
@@ -1137,7 +1138,8 @@
fire(this, 'open-fix-preview', await this.createFixPreview());
}
- async createSuggestEdit() {
+ async createSuggestEdit(e: MouseEvent) {
+ e.stopPropagation();
const line = await this.getCommentedCode();
this.messageText += `${USER_SUGGESTION_START_PATTERN}${line}${'\n```'}`;
}
@@ -1261,51 +1263,35 @@
}
}
- private async openDeleteCommentModal() {
- this.showConfirmDeleteModal = true;
- await this.updateComplete;
- await this.confirmDeleteModal?.showModal();
+ private openDeleteCommentModal() {
+ this.confirmDeleteModal?.showModal();
+ whenVisible(this.confirmDeleteDialog!, () => {
+ this.confirmDeleteDialog!.resetFocus();
+ });
}
private closeDeleteCommentModal() {
- this.showConfirmDeleteModal = false;
- this.confirmDeleteModal?.remove();
this.confirmDeleteModal?.close();
}
/**
* Deleting a *published* comment is an admin feature. It means more than just
* discarding a draft.
- *
- * TODO: Also move this into the comments-service.
- * TODO: Figure out a good reloading strategy when deleting was successful.
- * `this.comment = newComment` does not seem sufficient.
*/
// private, but visible for testing
- handleConfirmDeleteComment() {
- const dialog = this.confirmDeleteModal?.querySelector(
- '#confirmDeleteComment'
- ) as GrConfirmDeleteCommentDialog | null;
- if (!dialog || !dialog.message) {
+ async handleConfirmDeleteComment() {
+ if (!this.confirmDeleteDialog || !this.confirmDeleteDialog.message) {
throw new Error('missing confirm delete dialog');
}
assertIsDefined(this.changeNum, 'changeNum');
assertIsDefined(this.comment, 'comment');
- assertIsDefined(this.comment.patch_set, 'comment.patch_set');
- if (isDraftOrUnsaved(this.comment)) {
- throw new Error('Admin deletion is only for published comments.');
- }
- this.restApiService
- .deleteComment(
- this.changeNum,
- this.comment.patch_set,
- this.comment.id,
- dialog.message
- )
- .then(newComment => {
- this.closeDeleteCommentModal();
- this.comment = newComment;
- });
+
+ await this.getCommentsModel().deleteComment(
+ this.changeNum,
+ this.comment,
+ this.confirmDeleteDialog.message
+ );
+ this.closeDeleteCommentModal();
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 5b191c8..3390369 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -127,6 +127,10 @@
</div>
</div>
</gr-endpoint-decorator>
+ <dialog id="confirmDeleteModal" tabindex="-1">
+ <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog">
+ </gr-confirm-delete-comment-dialog>
+ </dialog>
`
);
});
@@ -166,6 +170,10 @@
</div>
</div>
</gr-endpoint-decorator>
+ <dialog id="confirmDeleteModal" tabindex="-1">
+ <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog">
+ </gr-confirm-delete-comment-dialog>
+ </dialog>
`
);
});
@@ -238,6 +246,10 @@
</div>
</div>
</gr-endpoint-decorator>
+ <dialog id="confirmDeleteModal" tabindex="-1">
+ <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog">
+ </gr-confirm-delete-comment-dialog>
+ </dialog>
`
);
});
@@ -336,6 +348,10 @@
</div>
</div>
</gr-endpoint-decorator>
+ <dialog id="confirmDeleteModal" tabindex="-1">
+ <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog">
+ </gr-confirm-delete-comment-dialog>
+ </dialog>
`
);
});
@@ -421,6 +437,10 @@
</div>
</div>
</gr-endpoint-decorator>
+ <dialog id="confirmDeleteModal" tabindex="-1">
+ <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog">
+ </gr-confirm-delete-comment-dialog>
+ </dialog>
`
);
});
@@ -503,7 +523,7 @@
assertIsDefined(element.confirmDeleteModal, 'confirmDeleteModal');
const dialog = queryAndAssert<GrConfirmDeleteCommentDialog>(
element.confirmDeleteModal,
- '#confirmDeleteComment'
+ '#confirmDeleteCommentDialog'
);
dialog.message = 'removal reason';
await element.updateComplete;
@@ -834,12 +854,12 @@
.initiallyCollapsed=${false}
></gr-comment>`
);
+ element.editing = true;
});
test('renders suggest fix button', () => {
assert.dom.equal(
queryAndAssert(element, 'gr-button.suggestEdit'),
/* HTML */ `<gr-button
- aria-disabled="false"
class="action suggestEdit"
link=""
role="button"
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
index b6512b3..285a41a 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.ts
@@ -75,6 +75,7 @@
override render() {
return html` <gr-dialog
confirm-label="Delete"
+ ?disabled=${this.message === ''}
@confirm=${this.handleConfirmTap}
@cancel=${this.handleCancelTap}
>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts
index bd84ac4..b7551c1 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_test.ts
@@ -7,6 +7,7 @@
import {fixture, html, assert} from '@open-wc/testing';
import {GrConfirmDeleteCommentDialog} from './gr-confirm-delete-comment-dialog';
import './gr-confirm-delete-comment-dialog';
+import {GrDialog} from '../gr-dialog/gr-dialog';
suite('gr-confirm-delete-comment-dialog tests', () => {
let element: GrConfirmDeleteCommentDialog;
@@ -17,7 +18,10 @@
);
});
- test('render', () => {
+ test('render', async () => {
+ element.message = 'Just cause';
+ await element.updateComplete;
+
// prettier and shadowDom string disagree about wrapping in <p> tag.
assert.shadowDom.equal(
element,
@@ -43,4 +47,13 @@
`
);
});
+
+ test('dialog is disabled when message is empty', async () => {
+ element.message = '';
+ await element.updateComplete;
+
+ assert.isTrue(
+ (element.shadowRoot!.querySelector('gr-dialog') as GrDialog).disabled
+ );
+ });
});
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index c8663e6..45eca40 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -18,8 +18,6 @@
import {CommentLinks, EmailAddress} from '../../../api/rest-api';
import {linkifyUrlsAndApplyRewrite} from '../../../utils/link-util';
import '../gr-account-chip/gr-account-chip';
-import {KnownExperimentId} from '../../../services/flags/flags';
-import {getAppContext} from '../../../services/app-context';
/**
* This element optionally renders markdown and also applies some regex
@@ -36,8 +34,6 @@
@state()
private repoCommentLinks: CommentLinks = {};
- private readonly flagsService = getAppContext().flagsService;
-
private readonly getConfigModel = resolve(this, configModelToken);
// Private const but used in tests.
@@ -198,16 +194,23 @@
text = htmlEscape(text).toString();
// Unescape block quotes '>'. This is slightly dangerous as '>' can be used
// in HTML fragments, but it is insufficient on it's own.
- text = text.replace(/(^|\n)>/g, '$1>');
+ for (;;) {
+ const newText = text.replace(
+ /(^|\n)((?:\s{0,3}>)*\s{0,3})>/g,
+ '$1$2>'
+ );
+ if (newText === text) {
+ break;
+ }
+ text = newText;
+ }
return text;
}
override updated() {
// Look for @mentions and replace them with an account-label chip.
- if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
- this.convertEmailsToAccountChips();
- }
+ this.convertEmailsToAccountChips();
}
private convertEmailsToAccountChips() {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 9acca0f..3187ada 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -15,14 +15,9 @@
import './gr-formatted-text';
import {GrFormattedText} from './gr-formatted-text';
import {createConfig} from '../../../test/test-data-generators';
-import {
- queryAndAssert,
- stubFlags,
- waitUntilObserved,
-} from '../../../test/test-utils';
+import {queryAndAssert, waitUntilObserved} from '../../../test/test-utils';
import {CommentLinks, EmailAddress} from '../../../api/rest-api';
import {testResolver} from '../../../test/common-test-setup';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
suite('gr-formatted-text tests', () => {
@@ -412,38 +407,7 @@
);
});
- test('does not handle @mentions if not enabled', async () => {
- stubFlags('isEnabled')
- .withArgs(KnownExperimentId.MENTION_USERS)
- .returns(false);
- element.content = '@someone@google.com';
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html" class="markdown-html">
- <p>
- @
- <a
- href="mailto:someone@google.com"
- rel="noopener"
- target="_blank"
- >
- someone@google.com
- </a>
- </p>
- </div>
- </marked-element>
- `
- );
- });
-
- test('handles @mentions if enabled', async () => {
- stubFlags('isEnabled')
- .withArgs(KnownExperimentId.MENTION_USERS)
- .returns(true);
+ test('handles @mentions', async () => {
element.content = '@someone@google.com';
await element.updateComplete;
@@ -470,9 +434,6 @@
});
test('does not handle @mentions that is part of a code block', async () => {
- stubFlags('isEnabled')
- .withArgs(KnownExperimentId.MENTION_USERS)
- .returns(true);
element.content = '`@`someone@google.com';
await element.updateComplete;
@@ -593,5 +554,27 @@
`
);
});
+
+ test('renders nested block quotes', async () => {
+ element.content = '> > > block quote';
+ await element.updateComplete;
+
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html" class="markdown-html">
+ <blockquote>
+ <blockquote>
+ <blockquote>
+ <p>block quote</p>
+ </blockquote>
+ </blockquote>
+ </blockquote>
+ </div>
+ </marked-element>
+ `
+ );
+ });
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
index af744c6..b1b66ad 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {PluginApi} from '../../../api/plugin';
-import {isDefined} from '../../../types/types';
import {HookApi, PluginElement} from '../../../api/hook';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -14,16 +13,29 @@
moduleName: string;
plugin: PluginApi;
pluginUrl?: URL;
- type?: string;
+ type?: EndpointType;
domHook?: HookApi<PluginElement>;
slot?: string;
}
+/**
+ * Plugin-provided custom components can affect content in extension
+ * points using one of following methods:
+ * - DECORATE: custom component is set with `content` attribute and may
+ * decorate (e.g. style) DOM element.
+ * - REPLACE: contents of extension point are replaced with the custom
+ * component.
+ */
+export enum EndpointType {
+ DECORATE = 'decorate',
+ REPLACE = 'replace',
+}
+
interface Options {
endpoint: string;
dynamicEndpoint?: string;
slot?: string;
- type?: string;
+ type?: EndpointType;
moduleName?: string;
domHook?: HookApi<PluginElement>;
}
@@ -125,43 +137,7 @@
* Get detailed information about modules registered with an extension
* endpoint.
*/
- getDetails(name: string, options?: Options): ModuleInfo[] {
- const type = options && options.type;
- const moduleName = options && options.moduleName;
- if (!this._endpoints.has(name)) {
- return [];
- } else {
- return this._endpoints
- .get(name)!
- .filter(
- (item: ModuleInfo) =>
- (!type || item.type === type) &&
- (!moduleName || moduleName === item.moduleName)
- );
- }
- }
-
- /**
- * Get detailed module names for instantiating at the endpoint.
- */
- getModules(name: string, options?: Options): string[] {
- const modulesData = this.getDetails(name, options);
- if (!modulesData.length) {
- return [];
- }
- return modulesData.map(m => m.moduleName);
- }
-
- /**
- * Get plugin URLs with element and module definitions.
- */
- getPlugins(name: string, options?: Options): URL[] {
- const modulesData = this.getDetails(name, options);
- if (!modulesData.length) {
- return [];
- }
- return Array.from(new Set(modulesData.map(m => m.pluginUrl))).filter(
- isDefined
- );
+ getDetails(name: string): ModuleInfo[] {
+ return this._endpoints.get(name) ?? [];
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
index 2ef86ed..ddba546 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.ts
@@ -5,7 +5,7 @@
*/
import '../../../test/common-test-setup';
import './gr-js-api-interface';
-import {GrPluginEndpoints} from './gr-plugin-endpoints';
+import {EndpointType, GrPluginEndpoints} from './gr-plugin-endpoints';
import {PluginApi} from '../../../api/plugin';
import {HookApi, HookCallback, PluginElement} from '../../../api/hook';
import {assert} from '@open-wc/testing';
@@ -39,7 +39,7 @@
suite('gr-plugin-endpoints tests', () => {
let instance: GrPluginEndpoints;
let decoratePlugin: PluginApi;
- let stylePlugin: PluginApi;
+ let replacePlugin: PluginApi;
let domHook: HookApi<PluginElement>;
setup(() => {
@@ -52,19 +52,19 @@
);
instance.registerModule(decoratePlugin, {
endpoint: 'my-endpoint',
- type: 'decorate',
+ type: EndpointType.DECORATE,
moduleName: 'decorate-module',
domHook,
});
window.Gerrit.install(
- plugin => (stylePlugin = plugin),
+ plugin => (replacePlugin = plugin),
'0.1',
- 'http://test.com/plugins/testplugin/static/style.js'
+ 'http://test.com/plugins/testplugin/static/replace.js'
);
- instance.registerModule(stylePlugin, {
+ instance.registerModule(replacePlugin, {
endpoint: 'my-endpoint',
- type: 'style',
- moduleName: 'style-module',
+ type: EndpointType.REPLACE,
+ moduleName: 'replace-module',
domHook,
});
});
@@ -75,75 +75,28 @@
moduleName: 'decorate-module',
plugin: decoratePlugin,
pluginUrl: decoratePlugin._url,
- type: 'decorate',
+ type: EndpointType.DECORATE,
domHook,
slot: undefined,
},
{
- moduleName: 'style-module',
- plugin: stylePlugin,
- pluginUrl: stylePlugin._url,
- type: 'style',
+ moduleName: 'replace-module',
+ plugin: replacePlugin,
+ pluginUrl: replacePlugin._url,
+ type: EndpointType.REPLACE,
domHook,
slot: undefined,
},
]);
});
- test('getDetails by type', () => {
- assert.deepEqual(
- instance.getDetails('my-endpoint', {endpoint: 'a-place', type: 'style'}),
- [
- {
- moduleName: 'style-module',
- plugin: stylePlugin,
- pluginUrl: stylePlugin._url,
- type: 'style',
- domHook,
- slot: undefined,
- },
- ]
- );
- });
-
- test('getDetails by module', () => {
- assert.deepEqual(
- instance.getDetails('my-endpoint', {
- endpoint: 'my-endpoint',
- moduleName: 'decorate-module',
- }),
- [
- {
- moduleName: 'decorate-module',
- plugin: decoratePlugin,
- pluginUrl: decoratePlugin._url,
- type: 'decorate',
- domHook,
- slot: undefined,
- },
- ]
- );
- });
-
- test('getModules', () => {
- assert.deepEqual(instance.getModules('my-endpoint'), [
- 'decorate-module',
- 'style-module',
- ]);
- });
-
- test('getPlugins URLs are unique', () => {
- assert.equal(decoratePlugin._url, stylePlugin._url);
- assert.deepEqual(instance.getPlugins('my-endpoint'), [decoratePlugin._url]);
- });
-
test('onNewEndpoint', () => {
const newModuleStub = sinon.stub();
instance.setPluginsReady();
instance.onNewEndpoint('my-endpoint', newModuleStub);
instance.registerModule(decoratePlugin, {
endpoint: 'my-endpoint',
- type: 'replace',
+ type: EndpointType.REPLACE,
moduleName: 'replace-module',
domHook,
});
@@ -151,7 +104,7 @@
moduleName: 'replace-module',
plugin: decoratePlugin,
pluginUrl: decoratePlugin._url,
- type: 'replace',
+ type: EndpointType.REPLACE,
domHook,
slot: undefined,
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
index 7170051..832b97e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.ts
@@ -13,7 +13,7 @@
import {GrAnnotationActionsInterface} from './gr-annotation-actions-js-api';
import {GrEventHelper} from '../../plugins/gr-event-helper/gr-event-helper';
import {GrPluginRestApi} from './gr-plugin-rest-api';
-import {GrPluginEndpoints} from './gr-plugin-endpoints';
+import {EndpointType, GrPluginEndpoints} from './gr-plugin-endpoints';
import {getPluginNameFromUrl, send} from './gr-api-utils';
import {GrReportingJsApi} from './gr-reporting-js-api';
import {EventType, PluginApi, TargetElement} from '../../../api/plugin';
@@ -38,22 +38,6 @@
import {GrPluginStyleApi} from './gr-plugin-style-api';
import {StylePluginApi} from '../../../api/styles';
-/**
- * Plugin-provided custom components can affect content in extension
- * points using one of following methods:
- * - DECORATE: custom component is set with `content` attribute and may
- * decorate (e.g. style) DOM element.
- * - REPLACE: contents of extension point are replaced with the custom
- * component.
- * - STYLE: custom component is a shared styles module that is inserted
- * into the extension point.
- */
-enum EndpointType {
- DECORATE = 'decorate',
- REPLACE = 'replace',
- STYLE = 'style',
-}
-
const PLUGIN_NAME_NOT_SET = 'NULL';
export type SendCallback = (response: unknown) => void;
@@ -94,18 +78,6 @@
return this._name;
}
- registerStyleModule(endpoint: string, moduleName: string) {
- console.warn(
- `The deprecated plugin API 'registerStyleModule()' was called with parameters '${endpoint}' and '${moduleName}'.`
- );
- this.report.trackApi(this, 'plugin', 'registerStyleModule');
- this.pluginEndpoints.registerModule(this, {
- endpoint,
- type: EndpointType.STYLE,
- moduleName,
- });
- }
-
/**
* Registers an endpoint for the plugin.
*/
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
index fd43869..dad802a 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
@@ -121,30 +121,18 @@
</div>
<slot></slot>
<nav>
- Page ${this.computePage(this.offset, this.itemsPerPage)}
+ Page ${this.computePage()}
<a
id="prevArrow"
- href=${this.computeNavLink(
- this.offset,
- -1,
- this.itemsPerPage,
- this.filter,
- this.path
- )}
+ href=${this.computeNavLink(-1)}
?hidden=${this.loading || this.offset === 0}
>
<gr-icon icon="chevron_left"></gr-icon>
</a>
<a
id="nextArrow"
- href=${this.computeNavLink(
- this.offset,
- 1,
- this.itemsPerPage,
- this.filter,
- this.path
- )}
- ?hidden=${this.hideNextArrow(this.loading, this.items)}
+ href=${this.computeNavLink(1)}
+ ?hidden=${this.hideNextArrow()}
>
<gr-icon icon="chevron_right"></gr-icon>
</a>
@@ -177,9 +165,11 @@
() => {
if (!this.isConnected || !this.path) return;
if (filter) {
+ // TODO: Use navigation service instead of `page.show()` directly.
page.show(`${this.path}/q/filter:${encodeURL(filter, false)}`);
return;
}
+ // TODO: Use navigation service instead of `page.show()` directly.
page.show(this.path);
},
REQUEST_DEBOUNCE_INTERVAL_MS
@@ -191,19 +181,13 @@
}
// private but used in test
- computeNavLink(
- offset: number,
- direction: number,
- itemsPerPage: number,
- filter: string | undefined,
- path = ''
- ) {
+ computeNavLink(direction: number) {
// Offset could be a string when passed from the router.
- offset = +(offset || 0);
- const newOffset = Math.max(0, offset + itemsPerPage * direction);
- let href = getBaseUrl() + path;
- if (filter) {
- href += '/q/filter:' + encodeURL(filter, false);
+ const offset = +(this.offset || 0);
+ const newOffset = Math.max(0, offset + this.itemsPerPage * direction);
+ let href = getBaseUrl() + (this.path ?? '');
+ if (this.filter) {
+ href += '/q/filter:' + encodeURL(this.filter, false);
}
if (newOffset > 0) {
href += `,${newOffset}`;
@@ -212,11 +196,9 @@
}
// private but used in test
- hideNextArrow(loading?: boolean, items?: unknown[]) {
- if (loading || !items || !items.length) {
- return true;
- }
- const lastPage = items.length < this.itemsPerPage + 1;
+ hideNextArrow() {
+ if (this.loading || !this.items?.length) return true;
+ const lastPage = this.items.length < this.itemsPerPage + 1;
return lastPage;
}
@@ -224,8 +206,8 @@
// to either support a decimal or make it go to the nearest
// whole number (e.g 3).
// private but used in test
- computePage(offset: number, itemsPerPage: number) {
- return offset / itemsPerPage + 1;
+ computePage() {
+ return this.offset / this.itemsPerPage + 1;
}
private handleFilterBindValueChanged(e: BindValueChangeEvent) {
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
index bbbef72..bf94e8f 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.ts
@@ -57,36 +57,25 @@
});
test('computeNavLink', () => {
- const offset = 25;
- const projectsPerPage = 25;
- let filter = 'test';
- const path = '/admin/projects';
+ element.offset = 25;
+ element.itemsPerPage = 25;
+ element.filter = 'test';
+ element.path = '/admin/projects';
stubBaseUrl('');
- assert.equal(
- element.computeNavLink(offset, 1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:test,50'
- );
+ assert.equal(element.computeNavLink(1), '/admin/projects/q/filter:test,50');
- assert.equal(
- element.computeNavLink(offset, -1, projectsPerPage, filter, path),
- '/admin/projects/q/filter:test'
- );
+ assert.equal(element.computeNavLink(-1), '/admin/projects/q/filter:test');
- assert.equal(
- element.computeNavLink(offset, 1, projectsPerPage, undefined, path),
- '/admin/projects,50'
- );
+ element.filter = undefined;
+ assert.equal(element.computeNavLink(1), '/admin/projects,50');
- assert.equal(
- element.computeNavLink(offset, -1, projectsPerPage, undefined, path),
- '/admin/projects'
- );
+ assert.equal(element.computeNavLink(-1), '/admin/projects');
- filter = 'plugins/';
+ element.filter = 'plugins/';
assert.equal(
- element.computeNavLink(offset, 1, projectsPerPage, filter, path),
+ element.computeNavLink(1),
'/admin/projects/q/filter:plugins%252F,50'
);
});
@@ -113,19 +102,19 @@
test('next button', async () => {
element.itemsPerPage = 25;
- let projects = new Array(26);
+ element.items = new Array(26);
+ element.loading = false;
await element.updateComplete;
- let loading;
- assert.isFalse(element.hideNextArrow(loading, projects));
- loading = true;
- assert.isTrue(element.hideNextArrow(loading, projects));
- loading = false;
- assert.isFalse(element.hideNextArrow(loading, projects));
- projects = [];
- assert.isTrue(element.hideNextArrow(loading, projects));
- projects = new Array(4);
- assert.isTrue(element.hideNextArrow(loading, projects));
+ assert.isFalse(element.hideNextArrow());
+ element.loading = true;
+ assert.isTrue(element.hideNextArrow());
+ element.loading = false;
+ assert.isFalse(element.hideNextArrow());
+ element.items = [];
+ assert.isTrue(element.hideNextArrow());
+ element.items = new Array(4);
+ assert.isTrue(element.hideNextArrow());
});
test('prev button', async () => {
@@ -186,20 +175,40 @@
test('next/prev links change when path changes', async () => {
const BRANCHES_PATH = '/path/to/branches';
const TAGS_PATH = '/path/to/tags';
- const computeNavLinkStub = sinon.stub(element, 'computeNavLink');
element.offset = 0;
element.itemsPerPage = 25;
element.filter = '';
element.path = BRANCHES_PATH;
await element.updateComplete;
- assert.equal(computeNavLinkStub.lastCall.args[4], BRANCHES_PATH);
+
+ assert.dom.equal(
+ queryAndAssert(element, 'nav a'),
+ /* HTML */ `
+ <a hidden="" href="${BRANCHES_PATH}" id="prevArrow">
+ <gr-icon icon="chevron_left"> </gr-icon>
+ </a>
+ `
+ );
+
element.path = TAGS_PATH;
await element.updateComplete;
- assert.equal(computeNavLinkStub.lastCall.args[4], TAGS_PATH);
+
+ assert.dom.equal(
+ queryAndAssert(element, 'nav a'),
+ /* HTML */ `
+ <a hidden="" href="${TAGS_PATH}" id="prevArrow">
+ <gr-icon icon="chevron_left"> </gr-icon>
+ </a>
+ `
+ );
});
test('computePage', () => {
- assert.equal(element.computePage(0, 25), 1);
- assert.equal(element.computePage(50, 25), 3);
+ element.offset = 0;
+ element.itemsPerPage = 25;
+ assert.equal(element.computePage(), 1);
+ element.offset = 50;
+ element.itemsPerPage = 25;
+ assert.equal(element.computePage(), 3);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
deleted file mode 100644
index 0e0f143..0000000
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * @license
- * Copyright 2016 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../styles/shared-styles';
-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} from '@polymer/decorators';
-import {IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior';
-import {findActiveElement} from '../../../utils/dom-util';
-import {fireEvent} from '../../../utils/event-util';
-import {getFocusableElements} from '../../../utils/focusable';
-
-const AWAIT_MAX_ITERS = 10;
-const AWAIT_STEP = 5;
-const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-overlay': GrOverlay;
- }
-}
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = IronOverlayMixin(
- PolymerElement,
- IronOverlayBehavior as IronOverlayBehavior
-);
-
-/**
- * @attr {Boolean} with-backdrop - inherited from IronOverlay
- * @attr {Boolean} always-on-top - inherited from IronOverlay
- * @attr {Boolean} no-cancel-on-esc-key - inherited from IronOverlay
- * @attr {Boolean} no-cancel-on-outside-click - inherited from IronOverlay
- * @attr {String} scroll-action - inherited from IronOverlay
- */
-@customElement('gr-overlay')
-export class GrOverlay extends base {
- 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
- */
-
- // private but used in test
- fullScreenOpen = false;
-
- // private but used in test
- _boundHandleClose: () => void = () => super.close();
-
- private returnFocusTo?: HTMLElement;
-
- override get _focusableNodes() {
- return Array.from(getFocusableElements(this));
- }
-
- constructor() {
- super();
- this.addEventListener('iron-overlay-closed', () => this._overlayClosed());
- this.addEventListener('iron-overlay-cancelled', () =>
- this._overlayClosed()
- );
- }
-
- override open() {
- this.returnFocusTo = findActiveElement(document, true) ?? undefined;
- window.addEventListener('popstate', this._boundHandleClose);
- return new Promise<void>((resolve, reject) => {
- super.open.apply(this);
- if (this._isMobile()) {
- fireEvent(this, 'fullscreen-overlay-opened');
- 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) {
- fireEvent(this, 'fullscreen-overlay-closed');
- this.fullScreenOpen = false;
- }
- if (this.returnFocusTo) {
- this.returnFocusTo.focus();
- this.returnFocusTo = undefined;
- }
- }
-
- /**
- * 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 = () => {
- setTimeout(() => {
- 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/elements/shared/gr-overlay/gr-overlay_html.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.ts
deleted file mode 100644
index f6818a5..0000000
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="shared-styles">
- :host {
- background: var(--dialog-background-color);
- border-radius: var(--border-radius);
- box-shadow: var(--elevation-level-5);
- }
-
- @media screen and (max-width: 50em) {
- :host {
- height: 100%;
- left: 0;
- position: fixed;
- right: 0;
- top: 0;
- border-radius: 0;
- box-shadow: none;
- }
- }
- </style>
- <slot></slot>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.ts b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.ts
deleted file mode 100644
index dc98745..0000000
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * @license
- * Copyright 2017 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import './gr-overlay';
-import {GrOverlay} from './gr-overlay';
-import {fixture, html, assert} from '@open-wc/testing';
-
-suite('gr-overlay tests', () => {
- let element: GrOverlay;
-
- setup(async () => {
- element = await fixture(html`<gr-overlay><div>content</div></gr-overlay>`);
- });
-
- test('render', async () => {
- await element.open();
- assert.shadowDom.equal(element, /* HTML */ ' <slot></slot> ');
- });
-
- test('popstate listener is attached on open and removed on close', () => {
- const addEventListenerStub = sinon.stub(window, 'addEventListener');
- const removeEventListenerStub = sinon.stub(window, 'removeEventListener');
- element.open();
- assert.isTrue(addEventListenerStub.called);
- assert.equal(addEventListenerStub.lastCall.args[0], 'popstate');
- assert.equal(
- addEventListenerStub.lastCall.args[1],
- element._boundHandleClose
- );
- element._overlayClosed();
- assert.isTrue(removeEventListenerStub.called);
- assert.equal(removeEventListenerStub.lastCall.args[0], 'popstate');
- assert.equal(
- removeEventListenerStub.lastCall.args[1],
- element._boundHandleClose
- );
- });
-
- test('events are fired on fullscreen view', async () => {
- const isMobileStub = sinon.stub(element, '_isMobile').returns(true as any);
- const openHandler = sinon.stub();
- const closeHandler = sinon.stub();
- element.addEventListener('fullscreen-overlay-opened', openHandler);
- element.addEventListener('fullscreen-overlay-closed', closeHandler);
-
- await element.open();
-
- assert.isTrue(isMobileStub.called);
- assert.isTrue(element.fullScreenOpen);
- assert.isTrue(openHandler.called);
-
- element._overlayClosed();
- assert.isFalse(element.fullScreenOpen);
- assert.isTrue(closeHandler.called);
- });
-
- test('events are not fired on desktop view', async () => {
- const isMobileStub = sinon.stub(element, '_isMobile').returns(false as any);
- const openHandler = sinon.stub();
- const closeHandler = sinon.stub();
- element.addEventListener('fullscreen-overlay-opened', openHandler);
- element.addEventListener('fullscreen-overlay-closed', closeHandler);
-
- await element.open();
-
- assert.isTrue(isMobileStub.called);
- assert.isFalse(element.fullScreenOpen);
- assert.isFalse(openHandler.called);
-
- element._overlayClosed();
- assert.isFalse(element.fullScreenOpen);
- assert.isFalse(closeHandler.called);
- });
-});
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 9f52517..e1e4ca5 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -5,7 +5,6 @@
*/
import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import '../gr-cursor-manager/gr-cursor-manager';
-import '../gr-overlay/gr-overlay';
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '../../../styles/shared-styles';
import {getAppContext} from '../../../services/app-context';
@@ -18,12 +17,11 @@
import {Key} from '../../../utils/dom-util';
import {ValueChangedEvent} from '../../../types/events';
import {fire} from '../../../utils/event-util';
-import {LitElement, css, html, nothing} from 'lit';
+import {LitElement, css, html} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {sharedStyles} from '../../../styles/shared-styles';
import {PropertyValues} from 'lit';
import {classMap} from 'lit/directives/class-map.js';
-import {KnownExperimentId} from '../../../services/flags/flags';
import {NumericChangeId, ServerInfo} from '../../../api/rest-api';
import {subscribe} from '../../lit/subscription-controller';
import {resolve} from '../../../models/dependency';
@@ -116,8 +114,6 @@
private readonly getChangeModel = resolve(this, changeModelToken);
- private readonly flagsService = getAppContext().flagsService;
-
private readonly restApiService = getAppContext().restApiService;
private readonly getConfigModel = resolve(this, configModelToken);
@@ -266,8 +262,6 @@
}
private renderMentionsDropdown() {
- if (!this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS))
- return nothing;
return html` <gr-autocomplete-dropdown
id="mentionsSuggestions"
.suggestions=${this.suggestions}
@@ -525,8 +519,6 @@
}
private isMentionsDropdownActive() {
- if (!this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS))
- return false;
return (
this.specialCharIndex !== -1 && this.text[this.specialCharIndex] === '@'
);
@@ -541,10 +533,8 @@
private computeSpecialCharIndex() {
const charAtCursor = this.text[this.textarea!.selectionStart - 1];
- if (this.flagsService.isEnabled(KnownExperimentId.MENTION_USERS)) {
- if (charAtCursor === '@' && this.specialCharIndex === -1) {
- this.specialCharIndex = this.getSpecialCharIndex(this.text);
- }
+ if (charAtCursor === '@' && this.specialCharIndex === -1) {
+ this.specialCharIndex = this.getSpecialCharIndex(this.text);
}
if (charAtCursor === ':' && this.specialCharIndex === -1) {
this.specialCharIndex = this.getSpecialCharIndex(this.text);
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
index 1460246..0400e85 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.ts
@@ -7,12 +7,7 @@
import './gr-textarea';
import {GrTextarea} from './gr-textarea';
import {ItemSelectedEvent} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
-import {
- pressKey,
- stubFlags,
- stubRestApi,
- waitUntil,
-} from '../../../test/test-utils';
+import {pressKey, stubRestApi, waitUntil} from '../../../test/test-utils';
import {fixture, html, assert} from '@open-wc/testing';
import {createAccountWithEmail} from '../../../test/test-data-generators';
import {Key} from '../../../utils/dom-util';
@@ -31,14 +26,16 @@
element,
/* HTML */ `<div id="hiddenText"></div>
<span id="caratSpan"> </span>
+ <gr-autocomplete-dropdown id="emojiSuggestions" is-hidden="">
+ </gr-autocomplete-dropdown>
<gr-autocomplete-dropdown
- id="emojiSuggestions"
+ id="mentionsSuggestions"
is-hidden=""
- style="position: fixed; top: 150px; left: 392.5px; box-sizing: border-box; max-height: 300px; max-width: 785px;"
+ role="listbox"
>
</gr-autocomplete-dropdown>
<iron-autogrow-textarea aria-disabled="false" focused="" id="textarea">
- </iron-autogrow-textarea> `,
+ </iron-autogrow-textarea>`,
{
// gr-autocomplete-dropdown sizing seems to vary between local & CI
ignoreAttributes: [
@@ -49,47 +46,6 @@
});
suite('mention users', () => {
- setup(async () => {
- stubFlags('isEnabled').returns(true);
- element.requestUpdate();
- await element.updateComplete;
- });
-
- test('renders', () => {
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <div id="hiddenText"></div>
- <span id="caratSpan"> </span>
- <gr-autocomplete-dropdown
- id="emojiSuggestions"
- is-hidden=""
- style="position: fixed; top: 478px; left: 321px; box-sizing: border-box; max-height: 956px; max-width: 642px;"
- >
- </gr-autocomplete-dropdown>
- <gr-autocomplete-dropdown
- id="mentionsSuggestions"
- is-hidden=""
- role="listbox"
- style="position: fixed; top: 478px; left: 321px; box-sizing: border-box; max-height: 956px; max-width: 642px;"
- >
- </gr-autocomplete-dropdown>
- <iron-autogrow-textarea
- focused=""
- aria-disabled="false"
- id="textarea"
- >
- </iron-autogrow-textarea>
- `,
- {
- // gr-autocomplete-dropdown sizing seems to vary between local & CI
- ignoreAttributes: [
- {tags: ['gr-autocomplete-dropdown'], attributes: ['style']},
- ],
- }
- );
- });
-
test('mentions selector is open when @ is typed & the textarea has focus', async () => {
// Needed for Safari tests. selectionStart is not updated when text is
// updated.
@@ -553,7 +509,7 @@
assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n ']);
});
- test('emoji dropdown is closed when iron-overlay-closed is fired', async () => {
+ test('emoji dropdown is closed when dropdown-closed is fired', async () => {
const resetSpy = sinon.spy(element, 'closeDropdown');
element.emojiSuggestions!.dispatchEvent(
new CustomEvent('dropdown-closed', {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
index 1c09372..b952a3d 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
@@ -16,7 +16,11 @@
DiffPreferencesInfo,
} from '../../../api/diff';
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
-import {countLines, diffClasses} from '../gr-diff/gr-diff-utils';
+import {
+ countLines,
+ diffClasses,
+ getResponsiveMode,
+} from '../gr-diff/gr-diff-utils';
import {GrDiffRow} from './gr-diff-row';
import '../gr-context-controls/gr-context-controls-section';
import '../gr-context-controls/gr-context-controls';
@@ -71,6 +75,10 @@
if (this.group.ignoredWhitespaceOnly) extras.push('ignoredWhitespaceOnly');
const pairs = this.getLinePairs();
+ const responsiveMode = getResponsiveMode(this.diffPrefs, this.renderPrefs);
+ const hideFileCommentButton =
+ this.diffPrefs?.show_file_comment_button === false ||
+ this.renderPrefs?.show_file_comment_button === false;
const body = html`
<tbody class=${diffClasses(...extras)}>
${this.renderContextControls()} ${this.renderMoveControls()}
@@ -86,6 +94,8 @@
.lineLength=${this.diffPrefs?.line_length ?? 80}
.tabSize=${this.diffPrefs?.tab_size ?? 2}
.unifiedDiff=${this.isUnifiedDiff()}
+ .responsiveMode=${responsiveMode}
+ .hideFileCommentButton=${hideFileCommentButton}
>
</gr-diff-row>
`;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
index d5f0e1b..c1b13ac 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text.ts
@@ -5,6 +5,7 @@
*/
import {LitElement, html, TemplateResult} from 'lit';
import {customElement, property} from 'lit/decorators.js';
+import {styleMap} from 'lit/directives/style-map.js';
import {diffClasses} from '../gr-diff/gr-diff-utils';
const SURROGATE_PAIR = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
@@ -114,7 +115,7 @@
}
const piece = html`<span
class=${diffClasses('tab')}
- style="tab-size: ${tabSize}; -moz-tab-size: ${tabSize};"
+ style=${styleMap({'tab-size': `${tabSize}`})}
>${TAB}</span
>`;
this.pieces.push(piece);
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
index aec7fe0..a0e7840 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-text_test.ts
@@ -75,20 +75,25 @@
test('tab wrapper style', async () => {
element.lineLimit = 100;
- for (const size of [1, 3, 8, 55]) {
- element.tabSize = size;
- await element.updateComplete;
- await check(
- '\t',
- /* HTML */ `
- <span
- class="gr-diff tab"
- style="tab-size: ${size}; -moz-tab-size: ${size};"
- >
- </span>
- `
- );
- }
+ element.tabSize = 4;
+ await check(
+ '\t',
+ /* HTML */ '<span class="gr-diff tab" style="tab-size:4;"></span>'
+ );
+ await check(
+ 'abc\t',
+ /* HTML */ 'abc<span class="gr-diff tab" style="tab-size:1;"></span>'
+ );
+
+ element.tabSize = 8;
+ await check(
+ '\t',
+ /* HTML */ '<span class="gr-diff tab" style="tab-size:8;"></span>'
+ );
+ await check(
+ 'abc\t',
+ /* HTML */ 'abc<span class="gr-diff tab" style="tab-size:5;"></span>'
+ );
});
test('tab wrapper insertion', async () => {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index f583c2e..87fd5ca 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -48,15 +48,21 @@
}, 0);
}
+export function isFileUnchanged(diff: DiffInfo) {
+ return !diff.content.some(
+ content => (content.a && !content.common) || (content.b && !content.common)
+ );
+}
+
export function getResponsiveMode(
- prefs: DiffPreferencesInfo,
+ prefs?: DiffPreferencesInfo,
renderPrefs?: RenderPreferences
): DiffResponsiveMode {
if (renderPrefs?.responsive_mode) {
return renderPrefs.responsive_mode;
}
// Backwards compatibility to the line_wrapping param.
- if (prefs.line_wrapping) {
+ if (prefs?.line_wrapping) {
return 'FULL_RESPONSIVE';
}
return 'NONE';
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
index 98b4586..25dc768 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
@@ -4,8 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {assert} from '@open-wc/testing';
+import {DiffInfo} from '../../../api/diff';
import '../../../test/common-test-setup';
-import {createElementDiff, formatText, createTabWrapper} from './gr-diff-utils';
+import {createDiff} from '../../../test/test-data-generators';
+import {
+ createElementDiff,
+ formatText,
+ createTabWrapper,
+ isFileUnchanged,
+} from './gr-diff-utils';
const LINE_BREAK_HTML = '<span class="gr-diff br"></span>';
@@ -156,4 +163,36 @@
expectTextLength('abc\tde\t', 10, 20);
expectTextLength('\t\t\t\t\t', 20, 100);
});
+
+ test('isFileUnchanged', () => {
+ let diff: DiffInfo = {
+ ...createDiff(),
+ content: [
+ {a: ['abcd'], ab: ['ef']},
+ {b: ['ancd'], a: ['xx']},
+ ],
+ };
+ assert.equal(isFileUnchanged(diff), false);
+ diff = {
+ ...createDiff(),
+ content: [{ab: ['abcd']}, {ab: ['ancd']}],
+ };
+ assert.equal(isFileUnchanged(diff), true);
+ diff = {
+ ...createDiff(),
+ content: [
+ {a: ['abcd'], ab: ['ef'], common: true},
+ {b: ['ancd'], ab: ['xx']},
+ ],
+ };
+ assert.equal(isFileUnchanged(diff), false);
+ diff = {
+ ...createDiff(),
+ content: [
+ {a: ['abcd'], ab: ['ef'], common: true},
+ {b: ['ancd'], ab: ['xx'], common: true},
+ ],
+ };
+ assert.equal(isFileUnchanged(diff), true);
+ });
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index 2097170..a0526ed 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -755,9 +755,9 @@
rule wins in case of same specificity.
*/
.trailing-whitespace,
- .content .trailing-whitespace,
+ .content .contentText .trailing-whitespace,
.trailing-whitespace .intraline,
- .content .trailing-whitespace .intraline {
+ .content .contentText .trailing-whitespace .intraline {
border-radius: var(--border-radius, 4px);
background-color: var(--diff-trailing-whitespace-indicator);
}
diff --git a/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.ts b/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.ts
deleted file mode 100644
index 30adedf..0000000
--- a/polygerrit-ui/app/mixins/iron-fit-mixin/iron-fit-mixin.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior';
-import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {Constructor} from '../../utils/common-util';
-
-// The mixinBehaviors clears all type information about superClass.
-// As a workaround, we define IronFitMixin with correct type.
-// Due to the following issues:
-// https://github.com/microsoft/TypeScript/issues/15870
-// https://github.com/microsoft/TypeScript/issues/9944
-// we have to import IronFitBehavior in the same file where IronFitMixin
-// is used. To ensure that this import can't be avoided, the second parameter
-// is added. Usage example:
-// class Element extends IronFitMixin(PolymerElement, IronFitBehavior as IronFitBehavior)
-// The code 'IronFitBehavior as IronFitBehavior' required, because IronFitBehavior
-// defined as an object, not as IronFitBehavior instance.
-
-export const IronFitMixin = <T extends Constructor<PolymerElement>>(
- superClass: T,
- _: IronFitBehavior
-): T & Constructor<IronFitBehavior> =>
- // TODO(TS): mixinBehaviors in some lib is returning: `new () => T` instead
- // which will fail the type check due to missing IronFitBehavior interface
- // eslint-disable-next-line
- mixinBehaviors([IronFitBehavior], superClass) as any;
diff --git a/polygerrit-ui/app/mixins/iron-overlay-mixin/iron-overlay-mixin.ts b/polygerrit-ui/app/mixins/iron-overlay-mixin/iron-overlay-mixin.ts
deleted file mode 100644
index 3625228..0000000
--- a/polygerrit-ui/app/mixins/iron-overlay-mixin/iron-overlay-mixin.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright 2020 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior';
-import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {Constructor} from '../../utils/common-util';
-
-// The mixinBehaviors clears all type information about superClass.
-// As a workaround, we define IronOverlayMixin with correct type.
-// Due to the following issues:
-// https://github.com/microsoft/TypeScript/issues/15870
-// https://github.com/microsoft/TypeScript/issues/9944
-// we have to import IronOverlayBehavior in the same file where IronOverlayMixin
-// is used. To ensure that this import can't be avoided, the second parameter
-// is added. Usage example:
-// class Element extends IronOverlayMixin(PolymerElement, IronOverlayBehavior as IronOverlayBehavior)
-// The code 'IronOverlayBehavior as IronOverlayBehavior' required, because
-// IronOverlayBehavior defined as an object, not as IronOverlayBehavior instance.
-export const IronOverlayMixin = <T extends Constructor<PolymerElement>>(
- superClass: T,
- _: IronOverlayBehavior
-): T & Constructor<IronOverlayBehavior> =>
- // TODO(TS): mixinBehaviors in some lib is returning: `new () => T`
- // instead which will fail the type check due to missing
- // IronOverlayBehavior interface
- // eslint-disable-next-line
- mixinBehaviors([IronOverlayBehavior], superClass) as any;
diff --git a/polygerrit-ui/app/models/change/change-model.ts b/polygerrit-ui/app/models/change/change-model.ts
index 8282a3f..446822f 100644
--- a/polygerrit-ui/app/models/change/change-model.ts
+++ b/polygerrit-ui/app/models/change/change-model.ts
@@ -12,6 +12,7 @@
PatchSetNum,
PreferencesInfo,
RevisionPatchSetNum,
+ PatchSetNumber,
} from '../../types/common';
import {DefaultBase} from '../../constants/constants';
import {combineLatest, from, fromEvent, Observable, forkJoin, of} from 'rxjs';
@@ -22,7 +23,6 @@
startWith,
switchMap,
} from 'rxjs/operators';
-import {RouterModel} from '../../services/router/router-model';
import {
computeAllPatchSets,
computeLatestPatchNum,
@@ -38,6 +38,13 @@
import {UserModel} from '../user/user-model';
import {define} from '../dependency';
import {isOwner} from '../../utils/change-util';
+import {
+ ChangeViewModel,
+ createChangeUrl,
+ createDiffUrl,
+ createEditUrl,
+} from '../views/change';
+import {NavigationService} from '../../elements/core/gr-navigation/gr-navigation';
export enum LoadingStatus {
NOT_LOADED = 'NOT_LOADED',
@@ -56,12 +63,6 @@
loadingStatus: LoadingStatus;
change?: ParsedChangeInfo;
/**
- * The name of the file user is viewing in the diff view mode. File path is
- * specified in the url or derived from the commentId.
- * Does not apply to change-view or edit-view.
- */
- diffPath?: string;
- /**
* The list of reviewed files, kept in the model because we want changes made
* in one view to reflect on other views without re-rendering the other views.
* Undefined means it's still loading and empty set means no files reviewed.
@@ -76,7 +77,7 @@
export function updateChangeWithEdit(
change?: ParsedChangeInfo,
edit?: EditInfo,
- routerPatchNum?: PatchSetNum
+ viewModelPatchNum?: PatchSetNum
): ParsedChangeInfo | undefined {
if (!change || !edit) return change;
assertIsDefined(edit.commit.commit, 'edit.commit.commit');
@@ -95,7 +96,7 @@
// which is still done in change-view. `_patchRange.patchNum` should
// eventually also be model managed, so we can reconcile these two code
// snippets into one location.
- if (routerPatchNum === undefined) {
+ if (viewModelPatchNum === undefined) {
change.current_revision = edit.commit.commit;
}
return change;
@@ -103,20 +104,20 @@
/**
* Derives the base patchset number from all the data that can potentially
- * influence it. Mostly just returns `routerBasePatchNum` or PARENT, but has
+ * influence it. Mostly just returns `viewModelBasePatchNum` or PARENT, but has
* some special logic when looking at merge commits.
*
- * NOTE: At the moment this returns just `routerBasePatchNum ?? PARENT`, see
+ * NOTE: At the moment this returns just `viewModelBasePatchNum ?? PARENT`, see
* TODO below.
*/
function computeBase(
- routerBasePatchNum: BasePatchSetNum | undefined,
+ viewModelBasePatchNum: BasePatchSetNum | undefined,
patchNum: RevisionPatchSetNum | undefined,
change: ParsedChangeInfo | undefined,
preferences: PreferencesInfo
): BasePatchSetNum {
- if (routerBasePatchNum && routerBasePatchNum !== PARENT) {
- return routerBasePatchNum;
+ if (viewModelBasePatchNum && viewModelBasePatchNum !== PARENT) {
+ return viewModelBasePatchNum;
}
if (!change || !patchNum) return PARENT;
@@ -129,7 +130,7 @@
// but we are not sure whether this was ever 100% working correctly. A
// major challenge is being able to select PARENT explicitly even if your
// preference for the default choice is FIRST_PARENT. <gr-file-list-header>
- // just uses `navigation.setUrl()` and the router does not have any
+ // just uses `navigation.setUrl()` and the view model does not have any
// way of forcing the basePatchSetNum to stick to PARENT without being
// altered back to FIRST_PARENT here.
// See also corresponding TODO in gr-settings-view.
@@ -150,7 +151,11 @@
export class ChangeModel extends Model<ChangeState> {
private change?: ParsedChangeInfo;
- private patchNum?: PatchSetNum;
+ private patchNum?: RevisionPatchSetNum;
+
+ private basePatchNum?: BasePatchSetNum;
+
+ private latestPatchNum?: PatchSetNumber;
public readonly change$ = select(
this.state$,
@@ -162,11 +167,6 @@
changeState => changeState.loadingStatus
);
- public readonly diffPath$ = select(
- this.state$,
- changeState => changeState?.diffPath
- );
-
public readonly reviewedFiles$ = select(
this.state$,
changeState => changeState?.reviewedFiles
@@ -178,8 +178,17 @@
public readonly labels$ = select(this.change$, change => change?.labels);
- public readonly latestPatchNum$ = select(this.change$, change =>
- computeLatestPatchNum(computeAllPatchSets(change))
+ public readonly revisions$ = select(
+ this.change$,
+ change => change?.revisions
+ );
+
+ public readonly patchsets$ = select(this.change$, change =>
+ computeAllPatchSets(change)
+ );
+
+ public readonly latestPatchNum$ = select(this.patchsets$, patchsets =>
+ computeLatestPatchNum(patchsets)
);
/**
@@ -192,57 +201,57 @@
public readonly patchNum$: Observable<RevisionPatchSetNum | undefined> =
select(
combineLatest([
- this.routerModel.state$,
+ this.viewModel.state$,
this.state$,
this.latestPatchNum$,
]).pipe(
/**
- * If you depend on both, router and change state, then you want to
- * filter out inconsistent state, e.g. router changeNum already updated,
- * change not yet reset to undefined.
+ * If you depend on both, view model and change state, then you want to
+ * filter out inconsistent state, e.g. view model changeNum already
+ * updated, change not yet reset to undefined.
*/
- filter(([routerState, changeState, _latestPatchN]) => {
+ filter(([viewModelState, changeState, _latestPatchN]) => {
const changeNum = changeState.change?._number;
- const routerChangeNum = routerState.changeNum;
- return changeNum === undefined || changeNum === routerChangeNum;
+ const viewModelChangeNum = viewModelState?.changeNum;
+ return changeNum === undefined || changeNum === viewModelChangeNum;
})
),
- ([routerState, _changeState, latestPatchN]) =>
- routerState?.patchNum || latestPatchN
+ ([viewModelState, _changeState, latestPatchN]) =>
+ viewModelState?.patchNum || latestPatchN
);
/**
* Emits the base patchset number. This is identical to the
- * `routerBasePatchNum$`, but has some special logic for merges.
+ * `viewModel.basePatchNum$`, but has some special logic for merges.
*
* Note that this selector can emit without the change being available!
*/
public readonly basePatchNum$: Observable<BasePatchSetNum> =
/**
- * If you depend on both, router and change state, then you want to filter
- * out inconsistent state, e.g. router changeNum already updated, change not
- * yet reset to undefined.
+ * If you depend on both, view model and change state, then you want to
+ * filter out inconsistent state, e.g. view model changeNum already
+ * updated, change not yet reset to undefined.
*/
select(
combineLatest([
- this.routerModel.state$,
+ this.viewModel.state$,
this.state$,
this.userModel.state$,
]).pipe(
- filter(([routerState, changeState, _]) => {
+ filter(([viewModelState, changeState, _]) => {
const changeNum = changeState.change?._number;
- const routerChangeNum = routerState.changeNum;
- return changeNum === undefined || changeNum === routerChangeNum;
+ const viewModelChangeNum = viewModelState?.changeNum;
+ return changeNum === undefined || changeNum === viewModelChangeNum;
}),
withLatestFrom(
- this.routerModel.routerBasePatchNum$,
+ this.viewModel.basePatchNum$,
this.patchNum$,
this.change$,
this.userModel.preferences$
)
),
- ([_, routerBasePatchNum, patchNum, change, preferences]) =>
- computeBase(routerBasePatchNum, patchNum, change, preferences)
+ ([_, viewModelBasePatchNum, patchNum, change, preferences]) =>
+ computeBase(viewModelBasePatchNum, patchNum, change, preferences)
);
public readonly isOwner$: Observable<boolean> = select(
@@ -257,13 +266,14 @@
);
constructor(
- private readonly routerModel: RouterModel,
+ private readonly navigation: NavigationService,
+ private readonly viewModel: ChangeViewModel,
private readonly restApiService: RestApiService,
private readonly userModel: UserModel
) {
super(initialState);
this.subscriptions = [
- combineLatest([this.routerModel.routerChangeNum$, this.reload$])
+ combineLatest([this.viewModel.changeNum$, this.reload$])
.pipe(
map(([changeNum, _]) => changeNum),
switchMap(changeNum => {
@@ -272,7 +282,7 @@
const edit = from(this.restApiService.getChangeEdit(changeNum));
return forkJoin([change, edit]);
}),
- withLatestFrom(this.routerModel.routerPatchNum$),
+ withLatestFrom(this.viewModel.patchNum$),
map(([[change, edit], patchNum]) =>
updateChangeWithEdit(change, edit, patchNum)
)
@@ -289,6 +299,12 @@
}),
this.change$.subscribe(change => (this.change = change)),
this.patchNum$.subscribe(patchNum => (this.patchNum = patchNum)),
+ this.basePatchNum$.subscribe(
+ basePatchNum => (this.basePatchNum = basePatchNum)
+ ),
+ this.latestPatchNum$.subscribe(
+ latestPatchNum => (this.latestPatchNum = latestPatchNum)
+ ),
combineLatest([this.patchNum$, this.changeNum$, this.userModel.loggedIn$])
.pipe(
switchMap(([patchNum, changeNum, loggedIn]) => {
@@ -303,11 +319,6 @@
];
}
- // Temporary workaround until path is derived in the model itself.
- updatePath(diffPath?: string) {
- this.updateState({diffPath});
- }
-
updateStateReviewedFiles(reviewedFiles: string[]) {
this.updateState({reviewedFiles});
}
@@ -372,6 +383,65 @@
return this.getState().change;
}
+ diffUrl(
+ diffView: {path: string; lineNum?: number},
+ patchNum = this.patchNum,
+ basePatchNum = this.basePatchNum
+ ) {
+ if (!this.change) return;
+ if (!this.patchNum) return;
+ return createDiffUrl({
+ change: this.change,
+ patchNum,
+ basePatchNum,
+ diffView,
+ });
+ }
+
+ navigateToDiff(
+ diffView: {path: string; lineNum?: number},
+ patchNum = this.patchNum,
+ basePatchNum = this.basePatchNum
+ ) {
+ const url = this.diffUrl(diffView, patchNum, basePatchNum);
+ if (!url) return;
+ this.navigation.setUrl(url);
+ }
+
+ changeUrl(openReplyDialog = false) {
+ if (!this.change) return;
+ const isLatest = this.latestPatchNum === this.patchNum;
+ return createChangeUrl({
+ change: this.change,
+ patchNum:
+ isLatest && this.basePatchNum === PARENT ? undefined : this.patchNum,
+ basePatchNum: this.basePatchNum,
+ openReplyDialog,
+ });
+ }
+
+ navigateToChange(openReplyDialog = false) {
+ const url = this.changeUrl(openReplyDialog);
+ if (!url) return;
+ this.navigation.setUrl(url);
+ }
+
+ editUrl(editView: {path: string; lineNum?: number}) {
+ if (!this.change) return;
+ return createEditUrl({
+ changeNum: this.change._number,
+ repo: this.change.project,
+ patchNum: this.patchNum,
+ editView,
+ });
+ }
+
+ navigateToEdit(editView: {path: string; lineNum?: number}) {
+ const url = this.editUrl(editView);
+ if (!url) return;
+ this.navigation.setUrl(url);
+ }
+
/**
* Check whether there is no newer patch than the latest patch that was
* available when this change was loaded.
diff --git a/polygerrit-ui/app/models/change/change-model_test.ts b/polygerrit-ui/app/models/change/change-model_test.ts
index a2fc7c9..c11c15b 100644
--- a/polygerrit-ui/app/models/change/change-model_test.ts
+++ b/polygerrit-ui/app/models/change/change-model_test.ts
@@ -9,6 +9,7 @@
import {
createChange,
createChangeMessageInfo,
+ createChangeViewState,
createEditInfo,
createParsedChange,
createRevision,
@@ -28,12 +29,13 @@
} from '../../types/common';
import {ParsedChangeInfo} from '../../types/types';
import {getAppContext} from '../../services/app-context';
-import {GerritView, routerModelToken} from '../../services/router/router-model';
import {ChangeState, LoadingStatus, updateChangeWithEdit} from './change-model';
import {ChangeModel} from './change-model';
import {assert} from '@open-wc/testing';
import {testResolver} from '../../test/common-test-setup';
import {userModelToken} from '../user/user-model';
+import {changeViewModelToken} from '../views/change';
+import {navigationToken} from '../../elements/core/gr-navigation/gr-navigation';
suite('updateChangeWithEdit() tests', () => {
test('undefined change', async () => {
@@ -83,7 +85,8 @@
setup(() => {
changeModel = new ChangeModel(
- testResolver(routerModelToken),
+ testResolver(navigationToken),
+ testResolver(changeViewModelToken),
getAppContext().restApiService,
testResolver(userModelToken)
);
@@ -121,10 +124,7 @@
assert.equal(stub.callCount, 0);
assert.isUndefined(state?.change);
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
state = await waitForLoadingStatus(LoadingStatus.LOADING);
assert.equal(stub.callCount, 1);
assert.isUndefined(state?.change);
@@ -140,10 +140,7 @@
const promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
promise.resolve(knownChange);
state = await waitForLoadingStatus(LoadingStatus.LOADED);
@@ -164,10 +161,7 @@
let promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
promise.resolve(knownChange);
state = await waitForLoadingStatus(LoadingStatus.LOADED);
@@ -178,8 +172,8 @@
_number: 123 as NumericChangeId,
};
promise = mockPromise<ParsedChangeInfo | undefined>();
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
+ testResolver(changeViewModelToken).setState({
+ ...createChangeViewState(),
changeNum: otherChange._number,
});
state = await waitForLoadingStatus(LoadingStatus.LOADING);
@@ -197,10 +191,7 @@
let promise = mockPromise<ParsedChangeInfo | undefined>();
const stub = stubRestApi('getChangeDetail').callsFake(() => promise);
let state: ChangeState;
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
promise.resolve(knownChange);
state = await waitForLoadingStatus(LoadingStatus.LOADED);
@@ -208,10 +199,7 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(undefined);
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: undefined,
- });
+ testResolver(changeViewModelToken).setState(undefined);
state = await waitForLoadingStatus(LoadingStatus.NOT_LOADED);
assert.equal(stub.callCount, 2);
assert.isUndefined(state?.change);
@@ -220,10 +208,7 @@
promise = mockPromise<ParsedChangeInfo | undefined>();
promise.resolve(knownChange);
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: knownChange._number,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
state = await waitForLoadingStatus(LoadingStatus.LOADED);
assert.equal(stub.callCount, 3);
assert.equal(state?.change, knownChange);
@@ -299,7 +284,7 @@
assert.equal(spy.lastCall.firstArg, PARENT);
// test update
- testResolver(routerModelToken).updateState({
+ testResolver(changeViewModelToken).updateState({
basePatchNum: 1 as PatchSetNumber,
});
assert.equal(spy.callCount, 2);
diff --git a/polygerrit-ui/app/models/change/files-model.ts b/polygerrit-ui/app/models/change/files-model.ts
index 07e64a2..0683af0 100644
--- a/polygerrit-ui/app/models/change/files-model.ts
+++ b/polygerrit-ui/app/models/change/files-model.ts
@@ -23,6 +23,9 @@
import {ChangeModel} from './change-model';
import {CommentsModel} from '../comments/comments-model';
+export type FileNameToNormalizedFileInfoMap = {
+ [name: string]: NormalizedFileInfo;
+};
export interface NormalizedFileInfo extends FileInfo {
__path: string;
// Compared to `FileInfo` these four props are required here.
@@ -115,7 +118,12 @@
export class FilesModel extends Model<FilesState> {
public readonly files$ = select(this.state$, state => state.files);
- public readonly filesWithUnmodified$ = select(
+ /**
+ * `files$` only includes the files that were modified. Here we also include
+ * all unmodified files that have comments with
+ * `status: FileInfoStatus.UNMODIFIED`.
+ */
+ public readonly filesIncludingUnmodified$ = select(
combineLatest([this.files$, this.commentsModel.commentedPaths$]),
([files, commentedPaths]) => addUnmodified(files, commentedPaths)
);
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index 6a5933c..ba43eb4 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -14,12 +14,13 @@
Replacement,
RunStatus,
} from '../../api/checks';
-import {PatchSetNumber} from '../../api/rest-api';
+import {PatchSetNumber, RevisionPatchSetNum} from '../../api/rest-api';
+import {CommentSide} from '../../constants/constants';
import {FixSuggestionInfo, FixReplacementInfo} from '../../types/common';
import {OpenFixPreviewEventDetail} from '../../types/events';
import {isDefined} from '../../types/types';
-import {PROVIDED_FIX_ID} from '../../utils/comment-util';
-import {assert, assertNever} from '../../utils/common-util';
+import {PROVIDED_FIX_ID, UnsavedInfo} from '../../utils/comment-util';
+import {assert, assertIsDefined, assertNever} from '../../utils/common-util';
import {fire} from '../../utils/event-util';
import {CheckResult, CheckRun, RunResult} from './checks-model';
@@ -86,6 +87,27 @@
}
}
+function pleaseFixMessage(result: RunResult) {
+ return `Please fix this ${result.category} reported by ${result.checkName}: ${result.summary}
+
+${result.message}`;
+}
+
+export function createPleaseFixComment(result: RunResult): UnsavedInfo {
+ const pointer = result.codePointers?.[0];
+ assertIsDefined(pointer, 'codePointer');
+ return {
+ __unsaved: true,
+ path: pointer.path,
+ patch_set: result.patchset as RevisionPatchSetNum,
+ side: CommentSide.REVISION,
+ line: pointer.range.end_line ?? pointer.range.start_line,
+ range: pointer.range,
+ message: pleaseFixMessage(result),
+ unresolved: true,
+ };
+}
+
export function createFixAction(
target: EventTarget,
result?: RunResult
diff --git a/polygerrit-ui/app/models/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
index a7b43ca..1fdf342 100644
--- a/polygerrit-ui/app/models/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -18,8 +18,10 @@
} from '../../types/common';
import {
addPath,
+ Comment,
DraftInfo,
isDraft,
+ isDraftOrUnsaved,
isDraftThread,
isUnsaved,
reportingDetails,
@@ -27,7 +29,6 @@
} from '../../utils/comment-util';
import {deepEqual} from '../../utils/deep-util';
import {select} from '../../utils/observable-util';
-import {RouterModel} from '../../services/router/router-model';
import {define} from '../dependency';
import {combineLatest, forkJoin, from, Observable, of} from 'rxjs';
import {fire, fireAlert, fireEvent} from '../../utils/event-util';
@@ -52,6 +53,7 @@
switchMap,
} from 'rxjs/operators';
import {isDefined} from '../../types/types';
+import {ChangeViewModel} from '../views/change';
export interface CommentState {
/** undefined means 'still loading' */
@@ -111,6 +113,35 @@
return nextState;
}
+/** Updates a single comment in a state. */
+export function updateComment(
+ state: CommentState,
+ comment: CommentInfo
+): CommentState {
+ if (!comment.path || !state.comments) {
+ return state;
+ }
+ const newCommentsAtPath = [...state.comments[comment.path]];
+ for (let i = 0; i < newCommentsAtPath.length; ++i) {
+ if (newCommentsAtPath[i].id === comment.id) {
+ // TODO: In "delete comment" the returned comment is missing some of the
+ // fields (for example patch_set), which would throw errors when
+ // rendering. Remove merging with the old comment, once that is fixed in
+ // server code.
+ newCommentsAtPath[i] = {...newCommentsAtPath[i], ...comment};
+
+ return {
+ ...state,
+ comments: {
+ ...state.comments,
+ [comment.path]: newCommentsAtPath,
+ },
+ };
+ }
+ }
+ throw new Error('Comment to be updated does not exist');
+}
+
// Private but used in tests.
export function setRobotComments(
state: CommentState,
@@ -384,7 +415,7 @@
private discardedDrafts: DraftInfo[] = [];
constructor(
- private readonly routerModel: RouterModel,
+ private readonly changeViewModel: ChangeViewModel,
private readonly changeModel: ChangeModel,
private readonly accountsModel: AccountsModel,
private readonly restApiService: RestApiService,
@@ -401,7 +432,7 @@
this.changeModel.patchNum$.subscribe(x => (this.patchNum = x))
);
this.subscriptions.push(
- this.routerModel.routerChangeNum$.subscribe(changeNum => {
+ this.changeViewModel.changeNum$.subscribe(changeNum => {
this.changeNum = changeNum;
this.setState({...initialState});
this.reloadAllComments();
@@ -621,6 +652,25 @@
this.report(Interaction.COMMENT_DISCARDED, draft);
}
+ async deleteComment(
+ changeNum: NumericChangeId,
+ comment: Comment,
+ reason: string
+ ) {
+ assertIsDefined(comment.patch_set, 'comment.patch_set');
+ if (isDraftOrUnsaved(comment)) {
+ throw new Error('Admin deletion is only for published comments.');
+ }
+
+ const newComment = await this.restApiService.deleteComment(
+ changeNum,
+ comment.patch_set,
+ comment.id,
+ reason
+ );
+ this.modifyState(s => updateComment(s, newComment));
+ }
+
private report(interaction: Interaction, comment: CommentBasics) {
const details = reportingDetails(comment);
this.reporting.reportInteraction(interaction, details);
diff --git a/polygerrit-ui/app/models/comments/comments-model_test.ts b/polygerrit-ui/app/models/comments/comments-model_test.ts
index 4db5d57..a689e42 100644
--- a/polygerrit-ui/app/models/comments/comments-model_test.ts
+++ b/polygerrit-ui/app/models/comments/comments-model_test.ts
@@ -6,11 +6,13 @@
import '../../test/common-test-setup';
import {
createAccountWithEmail,
+ createChangeViewState,
createDraft,
} from '../../test/test-data-generators';
import {
AccountInfo,
EmailAddress,
+ NumericChangeId,
Timestamp,
UrlEncodedCommentId,
} from '../../types/common';
@@ -19,16 +21,16 @@
import {
createComment,
createParsedChange,
- TEST_NUMERIC_CHANGE_ID,
} from '../../test/test-data-generators';
import {stubRestApi, waitUntil, waitUntilCalled} from '../../test/test-utils';
import {getAppContext} from '../../services/app-context';
-import {GerritView, routerModelToken} from '../../services/router/router-model';
import {PathToCommentsInfoMap} from '../../types/common';
import {changeModelToken} from '../change/change-model';
import {assert} from '@open-wc/testing';
import {testResolver} from '../../test/common-test-setup';
import {accountsModelToken} from '../accounts-model/accounts-model';
+import {ChangeComments} from '../../elements/diff/gr-comment-api/gr-comment-api';
+import {changeViewModelToken} from '../views/change';
suite('comments model tests', () => {
test('updateStateDeleteDraft', () => {
@@ -70,7 +72,7 @@
test('loads comments', async () => {
const model = new CommentsModel(
- testResolver(routerModelToken),
+ testResolver(changeViewModelToken),
testResolver(changeModelToken),
testResolver(accountsModelToken),
getAppContext().restApiService,
@@ -98,10 +100,7 @@
model.portedComments$.subscribe(c => (portedComments = c ?? {}))
);
- testResolver(routerModelToken).setState({
- view: GerritView.CHANGE,
- changeNum: TEST_NUMERIC_CHANGE_ID,
- });
+ testResolver(changeViewModelToken).setState(createChangeViewState());
testResolver(changeModelToken).updateStateChange(createParsedChange());
await waitUntilCalled(diffCommentsSpy, 'diffCommentsSpy');
@@ -131,7 +130,7 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
const model = new CommentsModel(
- testResolver(routerModelToken),
+ testResolver(changeViewModelToken),
testResolver(changeModelToken),
testResolver(accountsModelToken),
getAppContext().restApiService,
@@ -159,7 +158,7 @@
};
stubRestApi('getAccountDetails').returns(Promise.resolve(account));
const model = new CommentsModel(
- testResolver(routerModelToken),
+ testResolver(changeViewModelToken),
testResolver(changeModelToken),
testResolver(accountsModelToken),
getAppContext().restApiService,
@@ -187,4 +186,36 @@
});
await waitUntil(() => mentionedUsers.length === 0);
});
+
+ test('delete comment change is emitted', async () => {
+ const comment = createComment();
+ stubRestApi('deleteComment').returns(
+ Promise.resolve({
+ ...comment,
+ message: 'Comment is deleted',
+ })
+ );
+ const model = new CommentsModel(
+ testResolver(changeViewModelToken),
+ testResolver(changeModelToken),
+ testResolver(accountsModelToken),
+ getAppContext().restApiService,
+ getAppContext().reportingService
+ );
+
+ let changeComments: ChangeComments | undefined = undefined;
+ model.changeComments$.subscribe(x => (changeComments = x));
+ model.setState({
+ comments: {[comment.path!]: [comment]},
+ discardedDrafts: [],
+ });
+
+ model.deleteComment(123 as NumericChangeId, comment, 'Comment is deleted');
+
+ await waitUntil(
+ () =>
+ changeComments?.getAllCommentsForPath(comment.path!)[0].message ===
+ 'Comment is deleted'
+ );
+ });
});
diff --git a/polygerrit-ui/app/models/views/admin.ts b/polygerrit-ui/app/models/views/admin.ts
index 2ad95a2..3380637 100644
--- a/polygerrit-ui/app/models/views/admin.ts
+++ b/polygerrit-ui/app/models/views/admin.ts
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {GerritView} from '../../services/router/router-model';
+import {getBaseUrl} from '../../utils/url-util';
import {define} from '../dependency';
import {Model} from '../model';
import {ViewState} from './base';
@@ -21,6 +22,17 @@
offset?: number | string;
}
+export function createAdminUrl(state: Omit<AdminViewState, 'view'>) {
+ switch (state.adminView) {
+ case AdminChildView.REPOS:
+ return `${getBaseUrl()}/admin/repos`;
+ case AdminChildView.GROUPS:
+ return `${getBaseUrl()}/admin/groups`;
+ case AdminChildView.PLUGINS:
+ return `${getBaseUrl()}/admin/plugins`;
+ }
+}
+
export const adminViewModelToken = define<AdminViewModel>('admin-view-model');
export class AdminViewModel extends Model<AdminViewState | undefined> {
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 31d511a..a206037 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -10,6 +10,7 @@
BasePatchSetNum,
ChangeInfo,
PatchSetNumber,
+ EDIT,
} from '../../api/rest-api';
import {Tab} from '../../constants/constants';
import {GerritView} from '../../services/router/router-model';
@@ -26,18 +27,31 @@
import {Model} from '../model';
import {ViewState} from './base';
+export enum ChangeChildView {
+ OVERVIEW = 'OVERVIEW',
+ DIFF = 'DIFF',
+ EDIT = 'EDIT',
+}
+
export interface ChangeViewState extends ViewState {
view: GerritView.CHANGE;
+ childView: ChangeChildView;
changeNum: NumericChangeId;
repo: RepoName;
- edit?: boolean;
patchNum?: RevisionPatchSetNum;
basePatchNum?: BasePatchSetNum;
+ /** Refers to comment on COMMENTS tab in OVERVIEW. */
commentId?: UrlEncodedCommentId;
+
+ // TODO: Move properties that only apply to OVERVIEW into a submessage.
+
+ edit?: boolean;
/** This can be a string only for plugin provided tabs. */
tab?: Tab | string;
+ // TODO: Move properties that only apply to CHECKS tab into a submessage.
+
/** Checks related view state */
/** selected patchset for check runs (undefined=latest) */
@@ -61,6 +75,19 @@
forceReload?: boolean;
/** triggers opening the reply dialog */
openReplyDialog?: boolean;
+
+ /** These properties apply to the DIFF child view only. */
+ diffView?: {
+ path?: string;
+ lineNum?: number;
+ leftSide?: boolean;
+ };
+
+ /** These properties apply to the EDIT child view only. */
+ editView?: {
+ path?: string;
+ lineNum?: number;
+ };
}
/**
@@ -70,7 +97,7 @@
*/
export type CreateChangeUrlObject = Omit<
ChangeViewState,
- 'view' | 'changeNum' | 'repo'
+ 'view' | 'childView' | 'changeNum' | 'repo'
> & {
change: Pick<ChangeInfo, '_number' | 'project'>;
};
@@ -82,7 +109,9 @@
}
export function objToState(
- obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view'>
+ obj:
+ | (CreateChangeUrlObject & {childView: ChangeChildView})
+ | Omit<ChangeViewState, 'view'>
): ChangeViewState {
if (isCreateChangeUrlObject(obj)) {
return {
@@ -95,15 +124,26 @@
return {...obj, view: GerritView.CHANGE};
}
-export function createChangeUrl(
- obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view'>
-) {
- const state: ChangeViewState = objToState(obj);
- let range = getPatchRangeExpression(state);
- if (range.length) {
- range = '/' + range;
+export function createChangeViewUrl(state: ChangeViewState): string {
+ switch (state.childView) {
+ case ChangeChildView.OVERVIEW:
+ return createChangeUrl(state);
+ case ChangeChildView.DIFF:
+ return createDiffUrl(state);
+ case ChangeChildView.EDIT:
+ return createEditUrl(state);
}
- let suffix = `${range}`;
+}
+
+export function createChangeUrl(
+ obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view' | 'childView'>
+) {
+ const state: ChangeViewState = objToState({
+ ...obj,
+ childView: ChangeChildView.OVERVIEW,
+ });
+
+ let suffix = '';
const queries = [];
if (state.checksPatchset && state.checksPatchset > 0) {
queries.push(`checksPatchset=${state.checksPatchset}`);
@@ -136,7 +176,7 @@
suffix += ',edit';
}
if (state.commentId) {
- suffix = suffix + `/comments/${state.commentId}`;
+ suffix += `/comments/${state.commentId}`;
}
if (queries.length > 0) {
suffix += '?' + queries.join('&');
@@ -144,18 +184,99 @@
if (state.messageHash) {
suffix += state.messageHash;
}
- if (state.repo) {
- const encodedProject = encodeURL(state.repo, true);
- return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
- } else {
- return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
+
+ return `${createChangeUrlCommon(state)}${suffix}`;
+}
+
+export function createDiffUrl(
+ obj: CreateChangeUrlObject | Omit<ChangeViewState, 'view' | 'childView'>
+) {
+ const state: ChangeViewState = objToState({
+ ...obj,
+ childView: ChangeChildView.DIFF,
+ });
+
+ const path = `/${encodeURL(state.diffView?.path ?? '', true)}`;
+
+ let suffix = '';
+ // TODO: Move creating of comment URLs to a separate function. We are
+ // "abusing" the `commentId` property, which should only be used for pointing
+ // to comment in the COMMENTS tab of the OVERVIEW page.
+ if (state.commentId) {
+ suffix += `comment/${state.commentId}/`;
}
+
+ if (state.diffView?.lineNum) {
+ suffix += '#';
+ if (state.diffView?.leftSide) {
+ suffix += 'b';
+ }
+ suffix += state.diffView.lineNum;
+ }
+
+ return `${createChangeUrlCommon(state)}${path}${suffix}`;
+}
+
+export function createEditUrl(
+ obj: Omit<ChangeViewState, 'view' | 'childView'>
+): string {
+ const state: ChangeViewState = objToState({
+ ...obj,
+ childView: ChangeChildView.DIFF,
+ patchNum: obj.patchNum ?? EDIT,
+ });
+
+ const path = `/${encodeURL(state.editView?.path ?? '', true)}`;
+ const line = state.editView?.lineNum;
+ const suffix = line ? `#${line}` : '';
+
+ return `${createChangeUrlCommon(state)}${path},edit${suffix}`;
+}
+
+/**
+ * The shared part of creating a change URL between OVERVIEW, DIFF and EDIT
+ * child views.
+ */
+function createChangeUrlCommon(state: ChangeViewState) {
+ let range = getPatchRangeExpression(state);
+ if (range.length) range = '/' + range;
+
+ let repo = '';
+ if (state.repo) repo = `${encodeURL(state.repo, true)}/+/`;
+
+ return `${getBaseUrl()}/c/${repo}${state.changeNum}${range}`;
}
export const changeViewModelToken =
define<ChangeViewModel>('change-view-model');
export class ChangeViewModel extends Model<ChangeViewState | undefined> {
+ public readonly changeNum$ = select(this.state$, state => state?.changeNum);
+
+ public readonly patchNum$ = select(this.state$, state => state?.patchNum);
+
+ public readonly basePatchNum$ = select(
+ this.state$,
+ state => state?.basePatchNum
+ );
+
+ public readonly diffPath$ = select(
+ this.state$,
+ state => state?.diffView?.path
+ );
+
+ public readonly diffLine$ = select(
+ this.state$,
+ state => state?.diffView?.lineNum
+ );
+
+ public readonly diffLeftSide$ = select(
+ this.state$,
+ state => state?.diffView?.leftSide ?? false
+ );
+
+ public readonly childView$ = select(this.state$, state => state?.childView);
+
public readonly tab$ = select(this.state$, state => state?.tab);
public readonly checksPatchset$ = select(
diff --git a/polygerrit-ui/app/models/views/change_test.ts b/polygerrit-ui/app/models/views/change_test.ts
index b34a1ba..837e362 100644
--- a/polygerrit-ui/app/models/views/change_test.ts
+++ b/polygerrit-ui/app/models/views/change_test.ts
@@ -6,73 +6,145 @@
import {assert} from '@open-wc/testing';
import {
BasePatchSetNum,
- NumericChangeId,
RepoName,
RevisionPatchSetNum,
} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
import '../../test/common-test-setup';
-import {createChangeUrl, ChangeViewState} from './change';
-
-const STATE: ChangeViewState = {
- view: GerritView.CHANGE,
- changeNum: 1234 as NumericChangeId,
- repo: 'test' as RepoName,
-};
+import {
+ createChangeViewState,
+ createDiffViewState,
+ createEditViewState,
+} from '../../test/test-data-generators';
+import {
+ createChangeUrl,
+ createDiffUrl,
+ createEditUrl,
+ ChangeViewState,
+} from './change';
suite('change view state tests', () => {
test('createChangeUrl()', () => {
- const state: ChangeViewState = {...STATE};
+ const state: ChangeViewState = createChangeViewState();
- assert.equal(createChangeUrl(state), '/c/test/+/1234');
+ assert.equal(createChangeUrl(state), '/c/test-project/+/42');
state.patchNum = 10 as RevisionPatchSetNum;
- assert.equal(createChangeUrl(state), '/c/test/+/1234/10');
+ assert.equal(createChangeUrl(state), '/c/test-project/+/42/10');
state.basePatchNum = 5 as BasePatchSetNum;
- assert.equal(createChangeUrl(state), '/c/test/+/1234/5..10');
+ assert.equal(createChangeUrl(state), '/c/test-project/+/42/5..10');
state.messageHash = '#123';
- assert.equal(createChangeUrl(state), '/c/test/+/1234/5..10#123');
+ assert.equal(createChangeUrl(state), '/c/test-project/+/42/5..10#123');
});
test('createChangeUrl() baseUrl', () => {
window.CANONICAL_PATH = '/base';
- const state: ChangeViewState = {...STATE};
+ const state: ChangeViewState = createChangeViewState();
assert.equal(createChangeUrl(state).substring(0, 5), '/base');
window.CANONICAL_PATH = undefined;
});
test('createChangeUrl() checksRunsSelected', () => {
const state: ChangeViewState = {
- ...STATE,
+ ...createChangeViewState(),
checksRunsSelected: new Set(['asdf']),
};
assert.equal(
createChangeUrl(state),
- '/c/test/+/1234?checksRunsSelected=asdf'
+ '/c/test-project/+/42?checksRunsSelected=asdf'
);
});
test('createChangeUrl() checksResultsFilter', () => {
const state: ChangeViewState = {
- ...STATE,
+ ...createChangeViewState(),
checksResultsFilter: 'asdf.*qwer',
};
assert.equal(
createChangeUrl(state),
- '/c/test/+/1234?checksResultsFilter=asdf.*qwer'
+ '/c/test-project/+/42?checksResultsFilter=asdf.*qwer'
);
});
test('createChangeUrl() with repo name encoding', () => {
const state: ChangeViewState = {
- view: GerritView.CHANGE,
- changeNum: 1234 as NumericChangeId,
+ ...createChangeViewState(),
repo: 'x+/y+/z+/w' as RepoName,
};
- assert.equal(createChangeUrl(state), '/c/x%252B/y%252B/z%252B/w/+/1234');
+ assert.equal(createChangeUrl(state), '/c/x%252B/y%252B/z%252B/w/+/42');
+ });
+
+ test('createDiffUrl', () => {
+ const params: ChangeViewState = {
+ ...createDiffViewState(),
+ patchNum: 12 as RevisionPatchSetNum,
+ diffView: {path: 'x+y/path.cpp'},
+ };
+ assert.equal(
+ createDiffUrl(params),
+ '/c/test-project/+/42/12/x%252By/path.cpp'
+ );
+
+ window.CANONICAL_PATH = '/base';
+ assert.equal(createDiffUrl(params).substring(0, 5), '/base');
+ window.CANONICAL_PATH = undefined;
+
+ params.repo = 'test' as RepoName;
+ assert.equal(createDiffUrl(params), '/c/test/+/42/12/x%252By/path.cpp');
+
+ params.basePatchNum = 6 as BasePatchSetNum;
+ assert.equal(createDiffUrl(params), '/c/test/+/42/6..12/x%252By/path.cpp');
+
+ params.diffView = {
+ path: 'foo bar/my+file.txt%',
+ };
+ params.patchNum = 2 as RevisionPatchSetNum;
+ delete params.basePatchNum;
+ assert.equal(
+ createDiffUrl(params),
+ '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'
+ );
+
+ params.diffView = {
+ path: 'file.cpp',
+ lineNum: 123,
+ };
+ assert.equal(createDiffUrl(params), '/c/test/+/42/2/file.cpp#123');
+
+ params.diffView = {
+ path: 'file.cpp',
+ lineNum: 123,
+ leftSide: true,
+ };
+ assert.equal(createDiffUrl(params), '/c/test/+/42/2/file.cpp#b123');
+ });
+
+ test('diff with repo name encoding', () => {
+ const params: ChangeViewState = {
+ ...createDiffViewState(),
+ patchNum: 12 as RevisionPatchSetNum,
+ repo: 'x+/y' as RepoName,
+ diffView: {path: 'x+y/path.cpp'},
+ };
+ assert.equal(createDiffUrl(params), '/c/x%252B/y/+/42/12/x%252By/path.cpp');
+ });
+
+ test('createEditUrl', () => {
+ const params: ChangeViewState = {
+ ...createEditViewState(),
+ patchNum: 12 as RevisionPatchSetNum,
+ editView: {path: 'x+y/path.cpp' as RepoName, lineNum: 31},
+ };
+ assert.equal(
+ createEditUrl(params),
+ '/c/test-project/+/42/12/x%252By/path.cpp,edit#31'
+ );
+
+ window.CANONICAL_PATH = '/base';
+ assert.equal(createEditUrl(params).substring(0, 5), '/base');
+ window.CANONICAL_PATH = undefined;
});
});
diff --git a/polygerrit-ui/app/models/views/diff.ts b/polygerrit-ui/app/models/views/diff.ts
deleted file mode 100644
index 34f4ee7..0000000
--- a/polygerrit-ui/app/models/views/diff.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {
- NumericChangeId,
- RepoName,
- RevisionPatchSetNum,
- BasePatchSetNum,
- ChangeInfo,
-} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
-import {UrlEncodedCommentId} from '../../types/common';
-import {
- encodeURL,
- getBaseUrl,
- getPatchRangeExpression,
-} from '../../utils/url-util';
-import {define} from '../dependency';
-import {Model} from '../model';
-import {ViewState} from './base';
-
-export interface DiffViewState extends ViewState {
- view: GerritView.DIFF;
- changeNum: NumericChangeId;
- repo?: RepoName;
- commentId?: UrlEncodedCommentId;
- path?: string;
- patchNum?: RevisionPatchSetNum;
- basePatchNum?: BasePatchSetNum;
- lineNum?: number;
- leftSide?: boolean;
- commentLink?: boolean;
-}
-
-/**
- * This is a convenience type such that you can pass a `ChangeInfo` object
- * as the `change` property instead of having to set both the `changeNum` and
- * `project` properties explicitly.
- */
-export type CreateChangeUrlObject = Omit<
- DiffViewState,
- 'view' | 'changeNum' | 'project'
-> & {
- change: Pick<ChangeInfo, '_number' | 'project'>;
-};
-
-export function isCreateChangeUrlObject(
- state: CreateChangeUrlObject | Omit<DiffViewState, 'view'>
-): state is CreateChangeUrlObject {
- return !!(state as CreateChangeUrlObject).change;
-}
-
-export function objToState(
- obj: CreateChangeUrlObject | Omit<DiffViewState, 'view'>
-): DiffViewState {
- if (isCreateChangeUrlObject(obj)) {
- return {
- ...obj,
- view: GerritView.DIFF,
- changeNum: obj.change._number,
- repo: obj.change.project,
- };
- }
- return {...obj, view: GerritView.DIFF};
-}
-
-export function createDiffUrl(
- obj: CreateChangeUrlObject | Omit<DiffViewState, 'view'>
-) {
- const state: DiffViewState = objToState(obj);
- let range = getPatchRangeExpression(state);
- if (range.length) range = '/' + range;
-
- let suffix = `${range}/${encodeURL(state.path || '', true)}`;
-
- if (state.lineNum) {
- suffix += '#';
- if (state.leftSide) {
- suffix += 'b';
- }
- suffix += state.lineNum;
- }
-
- if (state.commentId) {
- suffix = `/comment/${state.commentId}` + suffix;
- }
-
- if (state.repo) {
- const encodedProject = encodeURL(state.repo, true);
- return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
- } else {
- return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
- }
-}
-
-export const diffViewModelToken = define<DiffViewModel>('diff-view-model');
-
-export class DiffViewModel extends Model<DiffViewState | undefined> {
- constructor() {
- super(undefined);
- }
-}
diff --git a/polygerrit-ui/app/models/views/diff_test.ts b/polygerrit-ui/app/models/views/diff_test.ts
deleted file mode 100644
index 7fab2a4..0000000
--- a/polygerrit-ui/app/models/views/diff_test.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {assert} from '@open-wc/testing';
-import {
- BasePatchSetNum,
- NumericChangeId,
- RepoName,
- RevisionPatchSetNum,
-} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
-import '../../test/common-test-setup';
-import {createDiffUrl, DiffViewState} from './diff';
-
-suite('diff view state tests', () => {
- test('createDiffUrl', () => {
- const params: DiffViewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- path: 'x+y/path.cpp' as RepoName,
- patchNum: 12 as RevisionPatchSetNum,
- repo: '' as RepoName,
- };
- assert.equal(createDiffUrl(params), '/c/42/12/x%252By/path.cpp');
-
- window.CANONICAL_PATH = '/base';
- assert.equal(createDiffUrl(params).substring(0, 5), '/base');
- window.CANONICAL_PATH = undefined;
-
- params.repo = 'test' as RepoName;
- assert.equal(createDiffUrl(params), '/c/test/+/42/12/x%252By/path.cpp');
-
- params.basePatchNum = 6 as BasePatchSetNum;
- assert.equal(createDiffUrl(params), '/c/test/+/42/6..12/x%252By/path.cpp');
-
- params.path = 'foo bar/my+file.txt%';
- params.patchNum = 2 as RevisionPatchSetNum;
- delete params.basePatchNum;
- assert.equal(
- createDiffUrl(params),
- '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'
- );
-
- params.path = 'file.cpp';
- params.lineNum = 123;
- assert.equal(createDiffUrl(params), '/c/test/+/42/2/file.cpp#123');
-
- params.leftSide = true;
- assert.equal(createDiffUrl(params), '/c/test/+/42/2/file.cpp#b123');
- });
-
- test('diff with repo name encoding', () => {
- const params: DiffViewState = {
- view: GerritView.DIFF,
- changeNum: 42 as NumericChangeId,
- path: 'x+y/path.cpp',
- patchNum: 12 as RevisionPatchSetNum,
- repo: 'x+/y' as RepoName,
- };
- assert.equal(createDiffUrl(params), '/c/x%252B/y/+/42/12/x%252By/path.cpp');
- });
-});
diff --git a/polygerrit-ui/app/models/views/edit.ts b/polygerrit-ui/app/models/views/edit.ts
deleted file mode 100644
index 3893576..0000000
--- a/polygerrit-ui/app/models/views/edit.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {
- EDIT,
- NumericChangeId,
- RepoName,
- RevisionPatchSetNum,
-} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
-import {
- encodeURL,
- getBaseUrl,
- getPatchRangeExpression,
-} from '../../utils/url-util';
-import {define} from '../dependency';
-import {Model} from '../model';
-import {ViewState} from './base';
-
-export interface EditViewState extends ViewState {
- view: GerritView.EDIT;
- changeNum: NumericChangeId;
- repo: RepoName;
- path: string;
- patchNum: RevisionPatchSetNum;
- lineNum?: number;
-}
-
-export function createEditUrl(state: Omit<EditViewState, 'view'>): string {
- if (state.patchNum === undefined) {
- state = {...state, patchNum: EDIT};
- }
- let range = getPatchRangeExpression(state);
- if (range.length) range = '/' + range;
-
- let suffix = `${range}/${encodeURL(state.path || '', true)}`;
- suffix += ',edit';
-
- if (state.lineNum) {
- suffix += '#';
- suffix += state.lineNum;
- }
-
- if (state.repo) {
- const encodedProject = encodeURL(state.repo, true);
- return `${getBaseUrl()}/c/${encodedProject}/+/${state.changeNum}${suffix}`;
- } else {
- return `${getBaseUrl()}/c/${state.changeNum}${suffix}`;
- }
-}
-
-export const editViewModelToken = define<EditViewModel>('edit-view-model');
-
-export class EditViewModel extends Model<EditViewState | undefined> {
- constructor() {
- super(undefined);
- }
-}
diff --git a/polygerrit-ui/app/models/views/edit_test.ts b/polygerrit-ui/app/models/views/edit_test.ts
deleted file mode 100644
index 00bc805..0000000
--- a/polygerrit-ui/app/models/views/edit_test.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {assert} from '@open-wc/testing';
-import {
- NumericChangeId,
- RepoName,
- RevisionPatchSetNum,
-} from '../../api/rest-api';
-import {GerritView} from '../../services/router/router-model';
-import '../../test/common-test-setup';
-import {createEditUrl, EditViewState} from './edit';
-
-suite('edit view state tests', () => {
- test('createEditUrl', () => {
- const params: EditViewState = {
- view: GerritView.EDIT,
- changeNum: 42 as NumericChangeId,
- repo: 'test-project' as RepoName,
- path: 'x+y/path.cpp' as RepoName,
- patchNum: 12 as RevisionPatchSetNum,
- lineNum: 31,
- };
- assert.equal(
- createEditUrl(params),
- '/c/test-project/+/42/12/x%252By/path.cpp,edit#31'
- );
-
- window.CANONICAL_PATH = '/base';
- assert.equal(createEditUrl(params).substring(0, 5), '/base');
- window.CANONICAL_PATH = undefined;
- });
-});
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 3df17c1..392b5a8 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -12,7 +12,6 @@
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-iconset-svg": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
- "@polymer/iron-overlay-behavior": "^3.0.3",
"@polymer/iron-selector": "^3.0.1",
"@polymer/marked-element": "^3.0.1",
"@polymer/paper-button": "^3.0.1",
diff --git a/polygerrit-ui/app/scripts/js/bundled-polymer-bridges.js b/polygerrit-ui/app/scripts/js/bundled-polymer-bridges.js
index 573b24a..494acd9 100644
--- a/polygerrit-ui/app/scripts/js/bundled-polymer-bridges.js
+++ b/polygerrit-ui/app/scripts/js/bundled-polymer-bridges.js
@@ -58,7 +58,3 @@
import 'polymer-bridges/polymer/lib/elements/custom-style_bridge.js';
import 'polymer-bridges/polymer/lib/legacy/mutable-data-behavior_bridge.js';
import 'polymer-bridges/polymer/polymer-legacy_bridge.js';
-
-// This is needed due to the Polymer.IronFocusablesHelper in gr-overlay.ts
-import 'polymer-bridges/iron-overlay-behavior/iron-focusables-helper_bridge.js';
-
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 1857bad..14fb253 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -50,12 +50,10 @@
agreementViewModelToken,
} from '../models/views/agreement';
import {ChangeViewModel, changeViewModelToken} from '../models/views/change';
-import {DiffViewModel, diffViewModelToken} from '../models/views/diff';
import {
DocumentationViewModel,
documentationViewModelToken,
} from '../models/views/documentation';
-import {EditViewModel, editViewModelToken} from '../models/views/edit';
import {GroupViewModel, groupViewModelToken} from '../models/views/group';
import {PluginViewModel, pluginViewModelToken} from '../models/views/plugin';
import {RepoViewModel, repoViewModelToken} from '../models/views/repo';
@@ -112,9 +110,7 @@
[agreementViewModelToken, () => new AgreementViewModel()],
[changeViewModelToken, () => new ChangeViewModel()],
[dashboardViewModelToken, () => new DashboardViewModel()],
- [diffViewModelToken, () => new DiffViewModel()],
[documentationViewModelToken, () => new DocumentationViewModel()],
- [editViewModelToken, () => new EditViewModel()],
[groupViewModelToken, () => new GroupViewModel()],
[pluginViewModelToken, () => new PluginViewModel()],
[repoViewModelToken, () => new RepoViewModel()],
@@ -139,9 +135,7 @@
resolver(agreementViewModelToken),
resolver(changeViewModelToken),
resolver(dashboardViewModelToken),
- resolver(diffViewModelToken),
resolver(documentationViewModelToken),
- resolver(editViewModelToken),
resolver(groupViewModelToken),
resolver(pluginViewModelToken),
resolver(repoViewModelToken),
@@ -154,7 +148,8 @@
changeModelToken,
() =>
new ChangeModel(
- resolver(routerModelToken),
+ resolver(navigationToken),
+ resolver(changeViewModelToken),
appContext.restApiService,
resolver(userModelToken)
),
@@ -163,7 +158,7 @@
commentsModelToken,
() =>
new CommentsModel(
- resolver(routerModelToken),
+ resolver(changeViewModelToken),
resolver(changeModelToken),
resolver(accountsModelToken),
appContext.restApiService,
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 572e107..29e9259 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -17,9 +17,7 @@
NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
CHECKS_DEVELOPER = 'UiFeature__checks_developer',
PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
- DIFF_RENDERING_LIT = 'UiFeature__diff_rendering_lit',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
SUGGEST_EDIT = 'UiFeature__suggest_edit',
- MENTION_USERS = 'UiFeature__mention_users',
- RENDER_MARKDOWN = 'UiFeature__render_markdown',
+ REBASE_CHAIN = 'UiFeature__rebase_chain',
}
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 6ad03a3..0d0c88f 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -143,7 +143,7 @@
import {ParsedChangeInfo} from '../../types/types';
import {ErrorCallback} from '../../api/rest';
import {addDraftProp, DraftInfo} from '../../utils/comment-util';
-import {BaseScheduler} from '../scheduler/scheduler';
+import {BaseScheduler, Scheduler} from '../scheduler/scheduler';
import {MaxInFlightScheduler} from '../scheduler/max-in-flight-scheduler';
import {escapeAndWrapSearchOperatorValue} from '../../utils/string-util';
@@ -270,6 +270,11 @@
function createWriteScheduler() {
return new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 5);
}
+
+function createSerializingScheduler() {
+ return new MaxInFlightScheduler<Response>(new BaseScheduler<Response>(), 1);
+}
+
export class GrRestApiServiceImpl implements RestApiService, Finalizable {
readonly _cache = siteBasedCache; // Shared across instances.
@@ -286,6 +291,9 @@
// Private, but used in tests.
readonly _restApiHelper: GrRestApiHelper;
+ // Used to serialize requests for certain RPCs
+ readonly _serialScheduler: Scheduler<Response>;
+
constructor(private readonly authService: AuthService) {
this._restApiHelper = new GrRestApiHelper(
this._cache,
@@ -294,6 +302,7 @@
createReadScheduler(),
createWriteScheduler()
);
+ this._serialScheduler = createSerializingScheduler();
}
finalize() {}
@@ -2232,11 +2241,13 @@
return this.getFromProjectLookup(changeNum).then(project => {
const encodedRepoName = project ? encodeURIComponent(project) + '~' : '';
const url = `/accounts/self/starred.changes/${encodedRepoName}${changeNum}`;
- return this._restApiHelper.send({
- method: starred ? HttpMethod.PUT : HttpMethod.DELETE,
- url,
- anonymizedUrl: '/accounts/self/starred.changes/*',
- });
+ return this._serialScheduler.schedule(() =>
+ this._restApiHelper.send({
+ method: starred ? HttpMethod.PUT : HttpMethod.DELETE,
+ url,
+ anonymizedUrl: '/accounts/self/starred.changes/*',
+ })
+ );
});
}
@@ -2686,7 +2697,7 @@
encodeURIComponent(repo) +
'/commits/' +
encodeURIComponent(commit),
- anonymizedUrl: '/projects/*/comments/*',
+ anonymizedUrl: '/projects/*/commits/*',
}) as Promise<CommitInfo | undefined>;
}
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index edde7a4..c3c1cb6 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -4,11 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {Observable} from 'rxjs';
-import {
- NumericChangeId,
- RevisionPatchSetNum,
- BasePatchSetNum,
-} from '../../types/common';
import {Model} from '../../models/model';
import {select} from '../../utils/observable-util';
import {define} from '../../models/dependency';
@@ -18,9 +13,7 @@
AGREEMENTS = 'agreements',
CHANGE = 'change',
DASHBOARD = 'dashboard',
- DIFF = 'diff',
DOCUMENTATION_SEARCH = 'documentation-search',
- EDIT = 'edit',
GROUP = 'group',
PLUGIN_SCREEN = 'plugin-screen',
REPO = 'repo',
@@ -31,9 +24,6 @@
export interface RouterState {
// Note that this router model view must be updated before view model state.
view?: GerritView;
- changeNum?: NumericChangeId;
- patchNum?: RevisionPatchSetNum;
- basePatchNum?: BasePatchSetNum;
}
export const routerModelToken = define<RouterModel>('router-model');
@@ -43,17 +33,6 @@
state => state.view
);
- readonly routerChangeNum$: Observable<NumericChangeId | undefined> = select(
- this.state$,
- state => state.changeNum
- );
-
- readonly routerPatchNum$: Observable<RevisionPatchSetNum | undefined> =
- select(this.state$, state => state.patchNum);
-
- readonly routerBasePatchNum$: Observable<BasePatchSetNum | undefined> =
- select(this.state$, state => state.basePatchNum);
-
constructor() {
super({});
}
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
index da61c41..9ca2213 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-config.ts
@@ -35,6 +35,8 @@
GO_TO_MERGED_CHANGES = 'GO_TO_MERGED_CHANGES',
GO_TO_ABANDONED_CHANGES = 'GO_TO_ABANDONED_CHANGES',
GO_TO_WATCHED_CHANGES = 'GO_TO_WATCHED_CHANGES',
+ GO_TO_REPOS = 'GO_TO_REPOS',
+ GO_TO_GROUPS = 'GO_TO_GROUPS',
CURSOR_NEXT_CHANGE = 'CURSOR_NEXT_CHANGE',
CURSOR_PREV_CHANGE = 'CURSOR_PREV_CHANGE',
@@ -167,6 +169,16 @@
{key: 'w', combo: ComboKey.G}
);
describe(
+ Shortcut.GO_TO_REPOS,
+ ShortcutSection.EVERYWHERE,
+ 'Go to Repositories',
+ {key: 'r', combo: ComboKey.G}
+ );
+ describe(Shortcut.GO_TO_GROUPS, ShortcutSection.EVERYWHERE, 'Go to Groups', {
+ key: 'g',
+ combo: ComboKey.G,
+ });
+ describe(
Shortcut.TOGGLE_CHECKBOX,
ShortcutSection.ACTIONS,
'Toggle checkbox',
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.ts b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
index 7a44e79..17b7461 100644
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.ts
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.ts
@@ -32,7 +32,7 @@
color: var(--deemphasized-text-color);
padding: var(--spacing-l);
}
- @media only screen and (max-width: 67em) {
+ @media only screen and (max-width: 70em) {
.main {
margin: var(--spacing-xxl) 0 var(--spacing-xxl) 15em;
}
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.ts b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
index b6e8f60..963b2a2 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.ts
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.ts
@@ -16,13 +16,16 @@
border-top: 1px solid transparent;
display: block;
padding: 0 var(--spacing-xl);
- }
- .navStyles li a {
- display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+ .navStyles li a {
+ display: block;
+ /* overflow and text-overflow are not inherited, must repeat them */
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
.navStyles .subsectionItem {
padding-left: var(--spacing-xxl);
}
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 86c6ba2..6dbda3e 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -480,10 +480,7 @@
--border-radius: 4px;
--line-length-indicator-color: #681da8;
- /* paper and iron component overrides */
- --iron-overlay-backdrop-background-color: black;
- --iron-overlay-backdrop-opacity: 0.32;
-
+ /* paper component overrides */
--paper-tooltip-delay-in: 200ms;
--paper-tooltip-delay-out: 0;
--paper-tooltip-duration-in: 0;
@@ -520,9 +517,6 @@
--paper-tooltip: {
font-size: var(--font-size-small);
};
- --iron-overlay-backdrop: {
- transition: none;
- };
}
`;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index ecae845..c6884a6 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -281,9 +281,6 @@
/* misc */
--line-length-indicator-color: #d7aefb;
- /* paper and iron component overrides */
- --iron-overlay-backdrop-background-color: white;
-
/* rules applied to html */
background-color: var(--view-background-color);
}
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 33d2753..aed58d8 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -16,8 +16,6 @@
import {
cleanupTestUtils,
getCleanupsCount,
- addIronOverlayBackdropStyleEl,
- removeIronOverlayBackdropStyleEl,
removeThemeStyles,
} from './test-utils';
import {safeTypesBridge} from '../utils/safe-types-util';
@@ -99,7 +97,6 @@
setup(() => {
testSetupTimestampMs = new Date().getTime();
- addIronOverlayBackdropStyleEl();
// If the following asserts fails - then window.stub is
// overwritten by some other code.
@@ -173,7 +170,6 @@
fixtureCleanup();
cleanupTestUtils();
checkGlobalSpace();
- removeIronOverlayBackdropStyleEl();
removeThemeStyles();
cancelAllTasks();
cleanUpStorage();
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 23a5794..f0a4cbe 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -105,7 +105,6 @@
import {EditRevisionInfo, ParsedChangeInfo} from '../types/types';
import {
DetailedLabelInfo,
- FileInfo,
QuickLabelInfo,
SubmitRequirementExpressionInfo,
SubmitRequirementResultInfo,
@@ -115,8 +114,8 @@
import {Category, RunStatus} from '../api/checks';
import {DiffInfo} from '../api/diff';
import {SearchViewState} from '../models/views/search';
-import {ChangeViewState} from '../models/views/change';
-import {EditViewState} from '../models/views/edit';
+import {ChangeChildView, ChangeViewState} from '../models/views/change';
+import {NormalizedFileInfo} from '../models/change/files-model';
const TEST_DEFAULT_EXPRESSION = 'label:Verified=MAX -label:Verified=MIN';
export const TEST_PROJECT_NAME: RepoName = 'test-project' as RepoName;
@@ -400,10 +399,15 @@
return messages;
}
-export function createFileInfo(): FileInfo {
+export function createFileInfo(
+ path = 'test-path/test-file.txt'
+): NormalizedFileInfo {
return {
size: 314,
size_delta: 7,
+ lines_deleted: 0,
+ lines_inserted: 0,
+ __path: path,
};
}
@@ -701,6 +705,7 @@
export function createChangeViewState(): ChangeViewState {
return {
view: GerritView.CHANGE,
+ childView: ChangeChildView.OVERVIEW,
changeNum: TEST_NUMERIC_CHANGE_ID,
repo: TEST_PROJECT_NAME,
};
@@ -716,12 +721,22 @@
};
}
-export function createEditViewState(): EditViewState {
+export function createEditViewState(): ChangeViewState {
return {
- view: GerritView.EDIT,
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.EDIT,
changeNum: TEST_NUMERIC_CHANGE_ID,
patchNum: EDIT,
- path: 'foo/bar.baz',
+ repo: TEST_PROJECT_NAME,
+ editView: {path: 'foo/bar.baz'},
+ };
+}
+
+export function createDiffViewState(): ChangeViewState {
+ return {
+ view: GerritView.CHANGE,
+ childView: ChangeChildView.DIFF,
+ changeNum: TEST_NUMERIC_CHANGE_ID,
repo: TEST_PROJECT_NAME,
};
}
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index ae6a6b4..c400d9c 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -122,24 +122,6 @@
ReturnType<F>
>;
-/**
- * Forcing an opacity of 0 onto the ironOverlayBackdrop is required, because
- * otherwise the backdrop stays around in the DOM for too long waiting for
- * an animation to finish.
- */
-export function addIronOverlayBackdropStyleEl() {
- const el = document.createElement('style');
- el.setAttribute('id', 'backdrop-style');
- document.head.appendChild(el);
- el.sheet!.insertRule('body { --iron-overlay-backdrop-opacity: 0; }');
-}
-
-export function removeIronOverlayBackdropStyleEl() {
- const el = document.getElementById('backdrop-style');
- if (!el?.parentNode) throw new Error('Backdrop style element not found.');
- el.parentNode?.removeChild(el);
-}
-
export function removeThemeStyles() {
// Do not remove the light theme, because it is only added once statically,
// not once per gr-app instantiation.
@@ -147,6 +129,30 @@
document.head.querySelector('#dark-theme')?.remove();
}
+function getActiveElement() {
+ return document.activeElement;
+}
+
+export function isFocusInsideElement(element: Element) {
+ // In Polymer 2 focused element either <paper-input> or nested
+ // native input <input> element depending on the current focus
+ // in browser window.
+ // For example, the focus is changed if the developer console
+ // get a focus.
+ let activeElement = getActiveElement();
+ while (activeElement) {
+ if (activeElement === element) {
+ return true;
+ }
+ if (activeElement.parentElement) {
+ activeElement = activeElement.parentElement;
+ } else {
+ activeElement = (activeElement.getRootNode() as ShadowRoot).host;
+ }
+ }
+ return false;
+}
+
export async function waitQueryAndAssert<E extends Element = Element>(
el: Element | null | undefined,
selector: string
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 7370c96..22bee0c 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -1511,3 +1511,8 @@
conflicts?: string[];
mergeable_into?: string[];
}
+
+export interface ChangeActionDialog extends HTMLElement {
+ resetFocus?(): void;
+ init?(): void;
+}
diff --git a/polygerrit-ui/app/types/types.ts b/polygerrit-ui/app/types/types.ts
index 8d55e36..557e3a0 100644
--- a/polygerrit-ui/app/types/types.ts
+++ b/polygerrit-ui/app/types/types.ts
@@ -10,7 +10,6 @@
AccountInfo,
BasePatchSetNum,
ChangeViewChangeInfo,
- CommitId,
CommitInfo,
EditPatchSet,
PatchSetNum,
@@ -28,11 +27,6 @@
requestAvailability(): void;
}
-export interface CommitRange {
- baseCommit: CommitId;
- commit: CommitId;
-}
-
export type {CoverageRange} from '../api/diff';
export {CoverageType} from '../api/diff';
diff --git a/polygerrit-ui/app/utils/admin-nav-util.ts b/polygerrit-ui/app/utils/admin-nav-util.ts
index 6f81d57..7916799 100644
--- a/polygerrit-ui/app/utils/admin-nav-util.ts
+++ b/polygerrit-ui/app/utils/admin-nav-util.ts
@@ -12,7 +12,7 @@
import {hasOwnProperty} from './common-util';
import {GerritView} from '../services/router/router-model';
import {MenuLink} from '../api/admin';
-import {AdminChildView} from '../models/views/admin';
+import {AdminChildView, createAdminUrl} from '../models/views/admin';
import {createGroupUrl, GroupDetailView} from '../models/views/group';
import {createRepoUrl, RepoDetailView} from '../models/views/repo';
@@ -20,7 +20,7 @@
{
name: 'Repositories',
noBaseUrl: true,
- url: '/admin/repos',
+ url: createAdminUrl({adminView: AdminChildView.REPOS}),
view: 'gr-repo-list' as GerritView,
viewableToAll: true,
},
@@ -28,7 +28,7 @@
name: 'Groups',
section: 'Groups',
noBaseUrl: true,
- url: '/admin/groups',
+ url: createAdminUrl({adminView: AdminChildView.GROUPS}),
view: 'gr-admin-group-list' as GerritView,
},
{
@@ -36,7 +36,7 @@
capability: 'viewPlugins',
section: 'Plugins',
noBaseUrl: true,
- url: '/admin/plugins',
+ url: createAdminUrl({adminView: AdminChildView.PLUGINS}),
view: 'gr-plugin-list' as GerritView,
},
];
diff --git a/polygerrit-ui/app/utils/attention-set-util.ts b/polygerrit-ui/app/utils/attention-set-util.ts
index 77834bd..4404e59 100644
--- a/polygerrit-ui/app/utils/attention-set-util.ts
+++ b/polygerrit-ui/app/utils/attention-set-util.ts
@@ -3,7 +3,12 @@
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {AccountInfo, ChangeInfo, ServerInfo} from '../types/common';
+import {
+ AccountInfo,
+ ChangeInfo,
+ DetailedLabelInfo,
+ ServerInfo,
+} from '../types/common';
import {ParsedChangeInfo} from '../types/types';
import {
getAccountTemplate,
@@ -13,6 +18,7 @@
} from './account-util';
import {CommentThread, isMentionedThread, isUnresolved} from './comment-util';
import {hasOwnProperty} from './common-util';
+import {getCodeReviewLabel} from './label-util';
export function canHaveAttention(account?: AccountInfo): boolean {
return !!account?._account_id && !isServiceUser(account);
@@ -101,9 +107,10 @@
/**
* Sort order:
* 1. The user themselves
- * 2. Human users in the attention set.
- * 3. Other human users.
- * 4. Service users.
+ * 2. Users in the attention set first.
+ * 3. Human users first.
+ * 4. Users that have voted first in this order of vote values:
+ * -2, -1, +2, +1, 0 or no vote.
*/
export function sortReviewers(
r1: AccountInfo,
@@ -117,7 +124,22 @@
}
const a1 = hasAttention(r1, change) ? 1 : 0;
const a2 = hasAttention(r2, change) ? 1 : 0;
- const s1 = isServiceUser(r1) ? -2 : 0;
- const s2 = isServiceUser(r2) ? -2 : 0;
- return a2 - a1 + s2 - s1;
+ if (a2 - a1 !== 0) return a2 - a1;
+
+ const s1 = isServiceUser(r1) ? -1 : 0;
+ const s2 = isServiceUser(r2) ? -1 : 0;
+ if (s2 - s1 !== 0) return s2 - s1;
+
+ const crLabel = getCodeReviewLabel(change?.labels ?? {}) as DetailedLabelInfo;
+ let v1 =
+ crLabel?.all?.find(vote => vote._account_id === r1._account_id)?.value ?? 0;
+ let v2 =
+ crLabel?.all?.find(vote => vote._account_id === r2._account_id)?.value ?? 0;
+ // We want negative votes getting a higher score than positive votes, so
+ // we choose 10 as a random number that is higher than all positive votes that
+ // are in use, and then add the absolute value of the vote to that.
+ // So -2 becomes 12.
+ if (v1 < 0) v1 = 10 - v1;
+ if (v2 < 0) v2 = 10 - v2;
+ return v2 - v1;
}
diff --git a/polygerrit-ui/app/utils/attention-set-util_test.ts b/polygerrit-ui/app/utils/attention-set-util_test.ts
index 8092a6e..5bd1924 100644
--- a/polygerrit-ui/app/utils/attention-set-util_test.ts
+++ b/polygerrit-ui/app/utils/attention-set-util_test.ts
@@ -6,9 +6,11 @@
import '../test/common-test-setup';
import {
createAccountDetailWithIdNameAndEmail,
+ createAccountWithId,
createChange,
createComment,
createCommentThread,
+ createParsedChange,
createServerInfo,
} from '../test/test-data-generators';
import {
@@ -22,9 +24,10 @@
getMentionedReason,
getReason,
hasAttention,
+ sortReviewers,
} from './attention-set-util';
import {DefaultDisplayNameConfig} from '../api/rest-api';
-import {AccountsVisibility} from '../constants/constants';
+import {AccountsVisibility, AccountTag} from '../constants/constants';
import {assert} from '@open-wc/testing';
const KERMIT: AccountInfo = {
@@ -101,6 +104,45 @@
assert.equal(getReason(config, OTHER_ACCOUNT, change), 'Added by kermit');
});
+ test('sortReviewers', () => {
+ const a1 = createAccountWithId(1);
+ a1.tags = [AccountTag.SERVICE_USER];
+ const a2 = createAccountWithId(2);
+ a2.tags = [AccountTag.SERVICE_USER];
+ const a3 = createAccountWithId(3);
+ const a4 = createAccountWithId(4);
+ const a5 = createAccountWithId(5);
+ const a6 = createAccountWithId(6);
+ const a7 = createAccountWithId(7);
+
+ const reviewers = [a1, a2, a3, a4, a5, a6, a7];
+ const change = {
+ ...createParsedChange(),
+ attention_set: {'6': {account: a6}},
+ labels: {
+ 'Code-Review': {
+ all: [
+ {...a2, value: 1},
+ {...a4, value: 1},
+ {...a5, value: -1},
+ ],
+ },
+ },
+ };
+ assert.sameOrderedMembers(
+ reviewers.sort((r1, r2) => sortReviewers(r1, r2, change, a7)),
+ [
+ a7, // self
+ a6, // is in the attention set
+ a5, // human user, has voted -1
+ a4, // human user, has voted +1
+ a3, // human user, has not voted
+ a2, // service user, has voted
+ a1, // service user, has not voted
+ ]
+ );
+ });
+
test('getMentionReason', () => {
let comment = {
...createComment(),
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 8af5beb..a92f0f8 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -598,3 +598,17 @@
.includes(account.email)
);
}
+
+export function findComment(
+ comments: {
+ [path: string]: (CommentInfo | DraftInfo)[];
+ },
+ commentId: UrlEncodedCommentId
+) {
+ if (!commentId) return undefined;
+ let comment;
+ for (const path of Object.keys(comments)) {
+ comment = comment || comments[path].find(c => c.id === commentId);
+ }
+ return comment;
+}
diff --git a/polygerrit-ui/app/utils/dom-util.ts b/polygerrit-ui/app/utils/dom-util.ts
index 23e3356..056238a 100644
--- a/polygerrit-ui/app/utils/dom-util.ts
+++ b/polygerrit-ui/app/utils/dom-util.ts
@@ -465,7 +465,7 @@
const path: EventTarget[] = e.composedPath() ?? [];
for (const el of path) {
if (!isElementTarget(el)) continue;
- if (el.tagName === 'GR-OVERLAY' || el.tagName === 'DIALOG') return true;
+ if (el.tagName === 'DIALOG') return true;
}
return false;
}
diff --git a/polygerrit-ui/app/utils/dom-util_test.ts b/polygerrit-ui/app/utils/dom-util_test.ts
index 3ad4438..fe185be 100644
--- a/polygerrit-ui/app/utils/dom-util_test.ts
+++ b/polygerrit-ui/app/utils/dom-util_test.ts
@@ -329,15 +329,6 @@
});
});
- test('suppress shortcut event from children of <gr-overlay>', async () => {
- const overlay = document.createElement('gr-overlay');
- const div = document.createElement('div');
- overlay.appendChild(div);
- await keyEventOn(div, e => {
- assert.isTrue(shouldSuppress(e));
- });
- });
-
test('suppress "enter" shortcut event from <gr-button>', async () => {
await keyEventOn(
document.createElement('gr-button'),
diff --git a/polygerrit-ui/app/utils/path-list-util.ts b/polygerrit-ui/app/utils/path-list-util.ts
index 1116123..b007d47 100644
--- a/polygerrit-ui/app/utils/path-list-util.ts
+++ b/polygerrit-ui/app/utils/path-list-util.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {SpecialFilePath, FileInfoStatus} from '../constants/constants';
-import {FileInfo} from '../types/common';
+import {FileInfo, FileNameToFileInfoMap} from '../types/common';
import {hasOwnProperty} from './common-util';
export function specialFilePathCompare(a: string, b: string) {
@@ -55,7 +55,7 @@
// In case there are files with comments on them but they are unchanged, then
// we explicitly displays the file to render the comments with Unchanged status
export function addUnmodifiedFiles(
- files: {[filename: string]: FileInfo},
+ files: FileNameToFileInfoMap,
commentedPaths: {[fileName: string]: boolean}
) {
if (!commentedPaths) return;
diff --git a/polygerrit-ui/app/utils/path-list-util_test.ts b/polygerrit-ui/app/utils/path-list-util_test.ts
index 50f5c0e..3c9e0d3 100644
--- a/polygerrit-ui/app/utils/path-list-util_test.ts
+++ b/polygerrit-ui/app/utils/path-list-util_test.ts
@@ -12,9 +12,9 @@
specialFilePathCompare,
truncatePath,
} from './path-list-util';
-import {FileInfo} from '../api/rest-api';
import {hasOwnProperty} from './common-util';
import {assert} from '@open-wc/testing';
+import {FileNameToFileInfoMap} from '../types/common';
suite('path-list-utl tests', () => {
test('special sort', () => {
@@ -117,7 +117,7 @@
'file1.txt': true,
};
- const files: {[filename: string]: FileInfo} = {
+ const files: FileNameToFileInfoMap = {
'file2.txt': {
status: FileInfoStatus.REWRITTEN,
size_delta: 10,
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 2f7cf3c..5a91fc0 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -161,7 +161,7 @@
dependencies:
"@polymer/polymer" "^3.0.0"
-"@polymer/iron-overlay-behavior@^3.0.0-pre.27", "@polymer/iron-overlay-behavior@^3.0.3":
+"@polymer/iron-overlay-behavior@^3.0.0-pre.27":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@polymer/iron-overlay-behavior/-/iron-overlay-behavior-3.0.3.tgz#29c198e19e05bb2bcf7d86d3c11848cb93301d00"
integrity sha512-Q/Fp0+uOQQ145ebZ7T8Cxl4m1tUKYjyymkjcL2rXUm+aDQGb1wA1M1LYxUF5YBqd+9lipE0PTIiYwA2ZL/sznA==
diff --git a/resources/com/google/gerrit/server/mail/Comment.soy b/resources/com/google/gerrit/server/mail/Comment.soy
index 98ab4b2..4b621b5 100644
--- a/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/resources/com/google/gerrit/server/mail/Comment.soy
@@ -77,13 +77,8 @@
{for $line, $index in $comment.lines}
{if $index == 0}
{if $comment.startLine != 0}
- {$comment.link}
+ {$comment.link}{sp}:{\n}
{/if}
-
- // Insert a space before the newline so that Gmail does not mistakenly
- // link the following line with the file link. See issue 9201.
- {sp}{\n}
-
{$comment.linePrefix}
{else}
{$comment.linePrefixEmpty}
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index 2f9561b..8e97ba7 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -39,6 +39,7 @@
cpp = text/x-c++src
cql = text/x-cassandra
cxx = text/x-c++src
+cu = text/x-c++src
cyp = application/x-cypher-query
cypher = application/x-cypher-query
c++ = text/x-c++src
diff --git a/tools/deps.bzl b/tools/deps.bzl
index d985a5c..ed8d65f5 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -114,8 +114,8 @@
maven_jar(
name = "commons-codec",
- artifact = "commons-codec:commons-codec:1.10",
- sha1 = "4b95f4897fa13f2cd904aee711aeafc0c5295cd8",
+ artifact = "commons-codec:commons-codec:1.15",
+ sha1 = "49d94806b6e3dc933dacbd8acb0fdbab8ebd1e5d",
)
# When upgrading commons-compress, also upgrade tukaani-xz
@@ -377,8 +377,8 @@
maven_jar(
name = "jsoup",
- artifact = "org.jsoup:jsoup:1.9.2",
- sha1 = "5e3bda828a80c7a21dfbe2308d1755759c2fd7b4",
+ artifact = "org.jsoup:jsoup:1.14.3",
+ sha1 = "c43a81e18e6d0eb71951aa031d55d5c293c531a6",
)
maven_jar(